0% encontró este documento útil (0 votos)
3 vistas204 páginas

Python Apav

El documento es un conjunto de apuntes sobre programación en Python, que abarca desde la historia y filosofía del lenguaje hasta conceptos avanzados como programación orientada a objetos. Incluye secciones sobre tipos de datos, estructuras de control, funciones y manejo de errores, así como ejemplos prácticos y bibliotecas relevantes. Está destinado a estudiantes de algorítmica y programación audiovisual.

Cargado por

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

Python Apav

El documento es un conjunto de apuntes sobre programación en Python, que abarca desde la historia y filosofía del lenguaje hasta conceptos avanzados como programación orientada a objetos. Incluye secciones sobre tipos de datos, estructuras de control, funciones y manejo de errores, así como ejemplos prácticos y bibliotecas relevantes. Está destinado a estudiantes de algorítmica y programación audiovisual.

Cargado por

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

Algorísmia i Programació Audiovisual

Apunts de progamació en Python

Professors de l’assignatura

mayo de 2023
Índice general

1. Introducción. 1
1.1. Historia y filosofía de Python. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1
1.2. Las bibliotecas de Python. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3
1.3. Implementaciones del Python; el CPyhton. . . . . . . . . . . . . . . . . . . . . . . . . 3

2. Modo interactivo, comandos básicos y emulación de MATLAB. 5


2.1. Modo interactivo de Pyhton; IDLE e IPython. . . . . . . . . . . . . . . . . . . . . . . 5
2.1.1. IPython. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5
2.2. Algunos comandos básicos de Python. . . . . . . . . . . . . . . . . . . . . . . . . . . . 7
2.3. Operadores de Python. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7
2.3.1. Operador asignación. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7
2.3.2. Operadores aritméticos. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8
2.3.3. Operadores bit a bit. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 9
2.3.4. Combinación de operador y asignación. . . . . . . . . . . . . . . . . . . . . . . 10
2.3.5. Operadores lógicos o booleanos. . . . . . . . . . . . . . . . . . . . . . . . . . . . 10
[Link]. Operadores relacionales. . . . . . . . . . . . . . . . . . . . . . . . . . . 11
[Link]. Operadores lógicos y cortocircuitos. . . . . . . . . . . . . . . . . . . . 11
[Link]. Operadores especiales: in e is. . . . . . . . . . . . . . . . . . . . . . . 12
2.4. Emulación de MATLAB usando Python. . . . . . . . . . . . . . . . . . . . . . . . . . . 12
2.4.1. Instalación de paquetes usando pip3. . . . . . . . . . . . . . . . . . . . . . . . 13
2.4.2. Visualización de una señal de audio. . . . . . . . . . . . . . . . . . . . . . . . . 14

3. Sistema de tipos de datos en Python. 17


3.1. Objetos, nombres y ¿variables?; el name binding. . . . . . . . . . . . . . . . . . . . . . 17
3.2. Tipos de datos en Python. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 19
3.2.1. Tipos simples. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 20
[Link]. Números enteros. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 20
[Link]. Números reales. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 21
[Link]. Números complejos. . . . . . . . . . . . . . . . . . . . . . . . . . . . . 21
3.2.2. Tipos compuestos. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 22
[Link]. Mutabilidad y hashabilidad. . . . . . . . . . . . . . . . . . . . . . . . . 22
[Link]. Cadenas de texto. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 25

i
ÍNDICE GENERAL

[Link]. Los tipos binarios, bytes y bytearray. . . . . . . . . . . . . . . . . . . 27


[Link]. Listas y tuplas. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 28
[Link]. Conjuntos y conjuntos congelados. . . . . . . . . . . . . . . . . . . . . 33
[Link]. Diccionarios. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 36
[Link]. Creación de objetos compuestos vacíos. . . . . . . . . . . . . . . . . . 37
[Link]. Iterables e iteradores; el tipo range. . . . . . . . . . . . . . . . . . . . 38
3.2.3. Tipos especiales. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 40
[Link]. Estructuras y clases. . . . . . . . . . . . . . . . . . . . . . . . . . . . . 40
[Link]. ndarray de numpy. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 41
3.3. Gestión de la memoria; el recolector de basura. . . . . . . . . . . . . . . . . . . . . . . 42

4. Estructuras de control de flujo (y semejantes). 45


4.1. Bloques de código. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 45
4.2. Estructuras condicionales. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 46
4.2.1. Evaluación de las condiciones en estructuras condicionales. . . . . . . . . . . . 47
4.3. Estructuras iterativas while y for. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 49
4.4. Comprensiones (comprehensions). . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 53
4.5. Otras estructuras de control. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 55

5. Programación procedimental y definición de funciones. 57


5.1. Subrutinas (funciones). . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 57
5.2. Convenio de nomenclatura de los identificadores. . . . . . . . . . . . . . . . . . . . . . 59
5.3. Pruebas unitarias. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 61
5.3.1. Pruebas unitarias en Python; la biblioteca doctest. . . . . . . . . . . . . . . . . 63
5.4. Manejo de errores y excepciones. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 66
5.4.1. Excepciones y rastro (traceback). . . . . . . . . . . . . . . . . . . . . . . . . . . 67
5.4.2. Elevación de excepciones. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 68
5.4.3. Captura de excepciones. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 68
5.4.4. El comando assert. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 70
5.5. Paso de argumentos a funciones. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 71
5.5.1. Argumentos inicializados y no inicializados en la definición. . . . . . . . . . . . 72
5.5.2. Argumentos posicionales y por clave en la invocación. . . . . . . . . . . . . . . 73
[Link]. Argumentos forzosamente posicionales o por clave. . . . . . . . . . . . 73
5.5.3. Argumentos de longitud variable en la definición. . . . . . . . . . . . . . . . . . 74
5.5.4. Desempaquetado de argumentos en la invocación. . . . . . . . . . . . . . . . . . 75
5.5.5. Paralelismo entre definición e invocación. . . . . . . . . . . . . . . . . . . . . . 75
5.6. Espacios de nombres y visibilidad. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 76
5.6.1. ¿Paso de argumentos por valor, por referencia...? Vinculación de nombres en la
función. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 77
5.6.2. Ámbitos y visibilidad. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 79
5.6.3. Acceso a objetos de ámbitos superiores. . . . . . . . . . . . . . . . . . . . . . . 81
[Link]. Acceso a objetos del ámbito global. . . . . . . . . . . . . . . . . . . . 82
[Link]. Acceso a objetos de un ámbito externo. . . . . . . . . . . . . . . . . . 83
[Link]. Unicidad de las vinculaciones en cada ámbito. . . . . . . . . . . . . . 83

ii
ÍNDICE GENERAL

6. Programación orientada a objeto. 85


6.1. Clases y objetos en Python. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 85
6.1.1. Clases y espacios de nombres. . . . . . . . . . . . . . . . . . . . . . . . . . . . . 86
6.1.2. Creación de objetos (y espacios de nombres). . . . . . . . . . . . . . . . . . . . 88
6.1.3. Métodos en clases y objetos. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 90
6.2. Ejemplo de aplicación: la clase Vector. . . . . . . . . . . . . . . . . . . . . . . . . . . . 91
6.2.1. Constructor de la clase. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 91
6.2.2. Representación textual: métodos __repr__() y __str__(). . . . . . . . . . . . . . 92
6.2.3. Acceso a los elementos: métodos __getitem__(), __setitem__() y __len__(). . . . 93
6.2.4. Sobrecarga de los operadores aritméticos. . . . . . . . . . . . . . . . . . . . . . 94
[Link]. Sobrecarga de operadores, reflejo e inmediatez. . . . . . . . . . . . . . 95
6.2.5. Otros métodos mágicos. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 96
[Link]. Sobrecarga de la invocación como función: __call__(). . . . . . . . . . 96
[Link]. Acceso a atributos del objeto: __getattr__(), __setattr__() y __delattr__. 96
[Link]. Clases hashables e igualdad: __hash__() y __eq__(). . . . . . . . . . . 96
[Link]. Cuidadito con el exceso de magia... . . . . . . . . . . . . . . . . . . . 96
6.3. Polimorfismo, herencia y tipado patoso. . . . . . . . . . . . . . . . . . . . . . . . . . . 97
6.3.1. Herencia. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 97
[Link]. Todos los objetos pertenecen a alguna clase. . . . . . . . . . . . . . . 99
6.3.2. El tipado patoso. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 100

7. Programación funcional. 105


7.1. Introducción a la programación funcional. . . . . . . . . . . . . . . . . . . . . . . . . . 105
7.1.1. Programación declarativa vs. imperativa. . . . . . . . . . . . . . . . . . . . . . 105
7.1.2. El paradigma de programación funcional. . . . . . . . . . . . . . . . . . . . . . 106
7.1.3. Características y alicientes de la programación funcional. . . . . . . . . . . . . 107
7.2. Elementos de programación funcional en Python. . . . . . . . . . . . . . . . . . . . . . 109
7.2.1. Iteradores y generadores. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 109
7.2.2. Funciones anónimas, la cláusula lambda. . . . . . . . . . . . . . . . . . . . . . . 112
7.2.3. Funciones de orden superior. . . . . . . . . . . . . . . . . . . . . . . . . . . . . 113
[Link]. Mapeos: la función map(). . . . . . . . . . . . . . . . . . . . . . . . . . 113
[Link]. Filtrados: la función filter(). . . . . . . . . . . . . . . . . . . . . . . 114
[Link]. Reducciones: la función reduce(). . . . . . . . . . . . . . . . . . . . . . 114
[Link]. Ordenación: la función sorted(). . . . . . . . . . . . . . . . . . . . . . 118
7.2.4. Decoración de funciones. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 119

iii
ÍNDICE GENERAL

8. Entrada/salida, cadenas con formato y datos binarios. 123


8.1. Entrada/salida por terminal. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 123
8.2. Entrada/salida por fichero. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 124
8.2.1. Comando open(). . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 125
8.2.2. Clases de tipo fichero. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 126
8.2.3. Cierre de los ficheros y recogida de basura; gestores de contexto. . . . . . . . . 129
8.3. Cadenas con formato. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 132
8.3.1. Cadenas de texto al estilo de printf(). . . . . . . . . . . . . . . . . . . . . . . . 133
8.3.2. Cadenas de texto usando el método format(). . . . . . . . . . . . . . . . . . . . 135
8.3.3. Cadenas-f (f-strings). . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 139
8.4. Empaquetado binario de los datos. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 140
8.4.1. Sexo, tamaño y alineado de los datos. . . . . . . . . . . . . . . . . . . . . . . . 142
8.4.2. Empaquetado de cadenas de texto. . . . . . . . . . . . . . . . . . . . . . . . . . 143
8.4.3. Conversión entre bytes y str. . . . . . . . . . . . . . . . . . . . . . . . . . . . . 144
8.4.4. Lectura y escritura de ficheros binarios. . . . . . . . . . . . . . . . . . . . . . . 145

9. Expresiones regulares. 151


9.1. Funciones y métodos del módulo re. . . . . . . . . . . . . . . . . . . . . . . . . . . . . 152
9.2. Algunas expresiones regulares sencillas. . . . . . . . . . . . . . . . . . . . . . . . . . . 154
9.3. Alternativa y agrupamiento. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 157
9.4. Repetición de sub-expresiones. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 158
9.5. Grupos avanzados. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 159
9.5.1. Acceso a partes concretas de una coincidencia. . . . . . . . . . . . . . . . . . . 160
9.5.2. Emulación de scanf(). . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 162

[Link], módulos, entornos virtuales y programas. 169


10.1. Ejecución de scripts. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 169
10.1.1. Las funciones (obsoletas) execfile(), runfile(). . . . . . . . . . . . . . . . . . . 170
10.1.2. El comando %run de ipython. . . . . . . . . . . . . . . . . . . . . . . . . . . . . 170
10.1.3. Importación de módulos y paquetes. . . . . . . . . . . . . . . . . . . . . . . . . 171
[Link]. El comando import. . . . . . . . . . . . . . . . . . . . . . . . . . . . . 172
10.2. Construcción de programas en Python. . . . . . . . . . . . . . . . . . . . . . . . . . . . 174
10.2.1. Ejecución de scripts desde el sistema operativo. . . . . . . . . . . . . . . . . . . 174
[Link]. Permisos de ejecución y el hashbang. . . . . . . . . . . . . . . . . . . . 176
10.2.2. Paso de argumentos en la línea de comandos. . . . . . . . . . . . . . . . . . . . 177
10.2.3. El interfaz en línea de comandos según POSIX y GNU. . . . . . . . . . . . . . 178
[Link]. Opciones y argumentos según POSIX. . . . . . . . . . . . . . . . . . . 178
[Link]. Extensiones y convenios de GNU. . . . . . . . . . . . . . . . . . . . . 179
[Link]. La sinopsis del programa. . . . . . . . . . . . . . . . . . . . . . . . . . 180
[Link]. Ejemplo de sinopsis conforme al estándar POSIX con las ampliaciones
de GNU. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 181

iv
ÍNDICE GENERAL

[Link]. Nombre típico de los argumentos de las opciones en GNU. . . . . . . 182


10.2.4. Gestión de opciones usando la biblioteca docopt. . . . . . . . . . . . . . . . . . 182
[Link]. Sintaxis de la sinopsis para su uso con docopt. . . . . . . . . . . . . . 183
[Link]. Análisis sintáctico de la invocación. . . . . . . . . . . . . . . . . . . . 184
[Link]. Acceso a los argumentos desde el programa. . . . . . . . . . . . . . . . 185
[Link]. Ejemplo completo de Naval Fate. . . . . . . . . . . . . . . . . . . . . . 187
10.2.5. Más información de docopt. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 188
10.3. Paquetes y módulos. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 188
10.3.1. El fichero __init__.py. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 189
10.3.2. Referencias absoluta y relativa a paquetes y módulos. . . . . . . . . . . . . . . 191
10.3.3. Directorios de búsqueda de los paquetes y módulos. . . . . . . . . . . . . . . . 192
10.3.4. Si todo esto le parece un follón... . . . . . . . . . . . . . . . . . . . . . . . . . . 193
10.4. Entornos virtuales. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 193
10.4.1. Entornos virtuales con venv. . . . . . . . . . . . . . . . . . . . . . . . . . . . . 194
10.4.2. Selección de las versiones de los paquetes y congelación del entorno virtual. . . 197
[Link]. Otros gestores de entornos virtuales. . . . . . . . . . . . . . . . . . . . 198

v
Capítulo 1

Introducción.

1.1. Historia y filosofía de Python.

Python es un lenguaje de programación de alto nivel creado por Guido van Rossum con el propósito
explícito de ser un lenguaje bonito que permita (incluso obligue) la creación de programas bonitos,
fáciles de entender y mantener, pero, al mismo tiempo, de altas prestaciones. Se trata de un lenguaje
interpretado multi-paradigma que permite, entre otras, programación procedimental, orientada a ob-
jeto y funcional. Con los años, Python ha visto crecer su popularidad hasta convertirse en uno de los
lenguajes más usados y demandados (y mejor pagados por la industria).
Van Rossum, cuyo nombre se suele abreviar como GvR, desarrolló los conceptos iniciales del lenguaje
a finales de los años 80 del siglo XX. La primera versión operativa se publicó en la década siguiente,
pero no se popularizó hasta la publicación de la versión 2 en los primeros años del milenio actual.
Desde entonces, Van Rossum ha ejercido como Benevolent Dictator for Life (abreviado como BDFL),
de manera que ha ejercido un férreo control sobre qué elementos se introducían en el lenguaje y cómo.
Este control ha garantizado una unidad y coherencia en el lenguaje de la que no gozan otros lenguajes,
como C/C++ o Java. La filosofía de Python se resume en el llamado Zen of Python . En él se enumeran
20 reglas1 que debe cumplir un programa para ser considerado pythónico2 . Por ejemplo, la primera
dice:

Beautiful is better than ugly.

Puede visualizar las 19 reglas escritas invocando la orden import this desde la consola de Python.
Una de las consecuencias del seguimiento estricto de esta filosofía es la creación de dos versiones del
lenguaje incompatibles entre sí: Python 2 y Python 3. Puede decirse que las dos primeras versiones,
Python 0 y Python 1, fueron más proyectos de investigación de Van Rossum y sus colegas que otra cosa.
Python 2, publicada en 2000, fue la primera versión realmente extendida y que popularizó Python.
Sin embargo, en el año 2008, se decidió realizar una gran revisión del lenguaje para hacerlo más
pythónico. Al contrario de lo realizado en otros lenguajes, como C y C++, se optó por un rediseño
completo del lenguaje que no garantizaría compatibilidad. De este modo se evitó el enmarañamiento
sufrido por otros lenguajes que sí han optado por mantener la compatibilidad. Pero, al mismo tiempo,
provocó la necesidad de seguir manteniendo el desarrollo en paralelo de dos lenguajes más o menos
independientes hasta que, con un aviso previo de varios años, Python 2 dejó de ser oficialmente
mantenido a principios del año 2020.
1
En verdad sólo hay 19 escritas. El autor del Zen de Python, Tim Peters, reservó la número 20 para hacerle la pelota
al BDFL, y que fuera éste quien la escribiera; pero, a día de hoy, el BDFL ha pasado mucho de hacerlo.
2
Pythónico es el neologismo creado por los pythonisos del pythoniverso para referirse al código que sigue la filosofía
de Python

1
CAPÍTULO 1. INTRODUCCIÓN.

Desde su introducción, el lenguaje se ha desarrollado en base a Python Enhancement Proposals (PEP) .


En esencia, un programador de la camarilla del BDFL realiza una propuesta de mejora del lenguaje,
que es sometida a discusión pública. Finalmente, el BDFL decide si la propuesta se incorpora al
lenguaje o no. Justamente, una de las últimas discusiones de este tipo se refirió al deseo de abandonar
el puesto de BDFL por parte de Van Rossum. En primera instancia, la discusión se centró en cuál
debía ser el modelo de gobierno del lenguaje después de su jubilación. Después de múltiples propuestas
(dictadura, república federal, democracia directa, etc.) el BDFL dictó que Python se regiría por un
directorio de cinco personas elegidas por un senado aristocrático de programadores.
Las versiones de Python se forman con tres números separados por punto. Por ejemplo, la versión más
reciente en febrero de 2021 es la 3.8.7, publicada el 21 de diciembre de 2020. El significado de los tres
números es el siguiente:

Versión del lenguaje:


El primero de los números es la versión del lenguaje, que no tiene por qué ser compatible con
las versiones anteriores. Así, la versión 3 no lo es con la aún muy popular versión 2.
Estas versiones sólo aparecen cuando se realizan modificaciones tan importantes que es impo-
sible garantizar la compatibilidad. Las primeras versiones, 0, 1 y 2, se sucedieron con bastante
rapidez. Entre la aparición de la versión 2 y la de la 3 transcurrieron ocho años; y no está
prevista la aparición de una nueva versión en muchos años (y ya llevamos desde 2008 con
Python 3).

Edición mayor:
Son versiones que introducen nuevas funcionalidades y optimizaciones. En general, se man-
tiene la compatibilidad con las ediciones mayores anteriores, aunque hay casos en que el
comportamiento varía ligeramente.
Hay ediciones mayores que introducen funcionalidades importantes en el lenguaje. Por ejem-
plo, la versión 3.6 introdujo un par que se estudiarán en este curso: cadenas formateadas
y subrayados en los literales numéricos; la 3.8 introdujo el operador morsa, los argumentos
exclusivamente posicionales y los argumentos autodocumentados en las cadenas formateadas;
y la 3.9 introduce el operador unión de diccionarios. Todo junto, puede decirse que la versión
3.9 constituye un avance importante respecto a la 3.5.
También en la aún no publicada 3.10 se introduce un elemento importante en la sintaxis
de Python: la estructura match, versión pythónica del switch de otros lenguajes. Durante
años, el BDFL se negó en redondo a introducir esta estructura, con muy bien fundamentados
argumentos. Sin embargo, desde su retiro y el paso del pythoniverso de una dictadura a una
aristocracia, las voces que la reclamaban se ha acabado imponiendo (aunque un primer vistazo
a la solución adoptada hace entender parte de los razonamientos de Guido van Rossum).
Sin embargo, otras ediciones afectan más a detalles de implementación que al lenguaje pro-
piamente dicho. Así, las versiones 3.5 y 3.7 apenas tuvieron impacto para el programador
estándar en Python.
Las ediciones mayores se presentan con una cadencia de unos dieciocho meses, aunque hay la
previsión de que sean anuales a partir de la versión 3.9. Su mantenimiento, en lo que respecta
a corrección de bugs y mejoras en la seguridad, se garantiza durante un periodo de cinco años.

Ediciones menores:
Durante la vida útil de cada edición mayor se publican ediciones en las que se corrigen
bugs o problemas de seguridad. Estas ediciones se publican cada vez que son necesarias,
aproximadamente cada tres meses. Es habitual que la edición más reciente no se corresponda
con la edición mayor (e, incluso, versión) más nueva. Así, la versión 3.8.7 se publicó el 21
de diciembre de 2021, mientras que la 3.9.1 lo hizo el día 7 del mismo mes; y en septiembre
de ese año se lanzó una actualización de la 3.5, casi un año después de que ya estuviera
disponible la 3.8.

2
1.2. LAS BIBLIOTECAS DE PYTHON.

La mayor limitación de Python es su pobre eficiencia computacional. De hecho, ésta nunca se ha


planteado como un objetivo del lenguaje. No obstante, la facilidad con la que interactúa con rutinas
escritas en otros lenguajes, particularmente C, hace que tal limitación sea mucho menos grave de lo que
pueda parecer. Justamente, uno de los objetivos de los creadores de Python es evitar la denominada
optimización prematura, esto es: sacrificar la legibilidad y mantenibilidad del código para obtener un
beneficio marginal en términos de eficiencia computacional. Según el criterio de los pythonisos del
pythoniverso lo más pythónico es escribir código lo más bonito y legible posible para, a continuación,
analizar en qué partes se emplean más recursos computacionales y, sólo si es necesario, optimizarlas o
reescribirlas en C.

1.2. Las bibliotecas de Python.

Una de las claves del éxito de Python radica en la abundancia de bibliotecas, también denominadas
módulos 3 que aumentan las capacidades del lenguaje. A menudo, estas bibliotecas están escritas en
C y, por tanto, son muy eficientes, Entre las más importantes, al menos para los propósitos de este
curso, cabe mencionar:

NumPy y SciPy: son dos bibliotecas de cálculo numérico y procesado de señal con funcionalidades
semejantes a MATLAB.

Matplotlib: biblioteca para generación de gráficas de alta calidad dotada de un interfaz seme-
jante a MATLAB.

SoundFile: biblioteca para lectura/escritura de ficheros de audio.

SoundDevice: biblioteca para grabación/reproducción de señales de audio.

1.3. Implementaciones del Python; el CPyhton.

Python es un lenguaje de programación desarrollado y mantenido por la organización sin ánimo


de lucro Python Software Foundation . Esta organización define la sintaxis y el comportamiento del
lenguaje y su librería estándar.
Al tratarse de un lenguaje interpretado, Python requiere del uso de un intérprete para poder ejecutar
las órdenes escritas en él. Del mismo modo que existen múltiples compiladores de Java o C/C++,
también existen múltiples intérpretes de Python. La Python Software Foundation proporciona uno,
el CPython , que, dada su relación con la gente que desarrolla el propio lenguaje, es considerada la
implementación de referencia del lenguaje.
CPython está escrito en C y Python (de ahí su nombre), y es lo que realmente descargamos e instalamos
cuando instalamos Python en un ordenador (porque, de hecho, Python no es ningún programa, sino
un lenguaje de programación).
CPython está tan íntimamente ligado a Python que alguna de sus características es considerada como
propia del lenguaje y no de la implementación. Por ejemplo, los diseñadores de CPython decidieron
simplificar su diseño imponiendo el GIL (Global Interpreter Lock), consistente en que, en cada mo-
mento, no puede ejecutarse más de una instrucción Python simultáneamente. Esta es una limitación
importante porque, en ordenadores con más de un procesador, no permite beneficiarse del uso de mul-
tithreading (procesado multi-hilo). El multithreading está soportado por Python y es útil para otras
muchas aplicaciones, pero no para reducir el tiempo de cálculo aprovechando el procesado paralelo.
3
También se usa el término librería como traducción chabacana del inglés library. Una library es una biblioteca; la
palabra castellana librería es la traducción de las inglesas bookstore (tienda de libros) o bookcase (mueble para guardarlos),
pero no se corresponde con library.

3
CAPÍTULO 1. INTRODUCCIÓN.

Sin embargo, el GIL es un detalle de la implementación CPython, no del lenguaje Python. Ha habido,
y seguro que habrá más, intentos de implementar un intérprete Python que no esté sometido a esta
limitación. Sin embargo, hasta la fecha (primavera de 2021), ninguno ha tenido éxito. Uno de los
ejemplos más conocidos es Unladen Swallow 4 , una iniciativa de Google que, después de varios años
de trabajo de desarrollo, fue abandonado sin ningún éxito destacable.

Otras implementaciones de Python.

Aparte de CPython, las dos implementaciones de Python más populares son Cython y PyPy . Ambas
surgen con el objetivo de ser alternativas eficientes computacionalmente a CPython.
Cython es un compilador cruzado a C: en lugar de interpretar el código Python generando instrucciones
de código máquina que puedan ser ejecutadas por el ordenador, genera código C que debe ser compilado
para poderse ejecutar. El principal aliciente es que el código C produce programas mucho más eficientes
en términos de tiempo de ejecución.
PyPy es un intérprete que compila el código Python a código máquina conforme lo interpreta. El
objetivo es que sólo sea necesario interpretar y compilar el código la primera vez que se ejecuta,
usando el código ya compilado las siguientes.

4
Unladen Swallow: Golondrina sin carga, en inglés; nunca quedó claro si europea o africana.

4
Capítulo 2

Modo interactivo, comandos básicos y


emulación de MATLAB.

2.1. Modo interactivo de Pyhton; IDLE e IPython.

Al ejecutar python3 en la línea de comandos se abre un interfaz de comandos semejante al usado por
otros programas, como MATLAB, y que permite la ejecución interactiva de Python. Es el programa
IDLE (inútil u ocioso en inglés), cuyo nombre, según Guido van Rossum, es el acrónimo de Interactive
DeveLopment Environment; aunque es más probable que sea un reflejo de su parquedad y limitaciones,
y seguramente un homenaje al miembro de Monty Python Eric Idle .
El modo interactivo permite realizar operaciones de manera semejante a cómo se harían con una
calculadora. Por ejemplo, la suma de dos números se indica con el operador habitual (+):

>>> 45 + 32.7
77.7

El resultado de la última operación realizada se almacena en la variable especial _, análoga a la variable


ans de MATLAB:

>>> _
77.7

2.1.1. IPython.

La parquedad de IDLE hace recomendable la búsqueda de alternativas más amigables. En modo gráfico
existen muchas de ellas, como PyCharm , Jupyter o Spyder . Todas ellas son entornos de desarrollo
integrados (IDEs) que, además de proporcionar el modo interactivo, también permiten editar y depurar
programas, refactorizarlos, mantener sus versiones, etc. Un problema de estos IDEs es, justamente,
que permiten automatizar muchas tareas, enmascarando el funcionamiento profundo del sistema.
En este curso se usará un interfaz de comandos en modo texto. En concreto, el que se ha convertido
en un estándar de facto: IPython . Aunque el nombre del programa es ipython, para garantizar que la
versión ejecutada es Python 3, debe emplearse el comando ipython3.
Las principales características de ipython3 por las que se primará su uso por encima de IDLE son:

Sangrado automático de las líneas al entrar o salir de un bloque lógico.

5
CAPÍTULO 2. MODO INTERACTIVO, COMANDOS BÁSICOS Y EMULACIÓN DE MATLAB.

Realce sintáctico que indica, con un código de colores, la función de las distintas palabras:
nombres, valores, comandos, funciones, palabras reservadas, etc.

Autocompletado inteligente usando el tabulador. Aunque también IDLE proporciona autocom-


pletado, el de ipython3 es mucho más potente, permitiendo, por ejemplo, completar el nombre
de ficheros incluidos en cadenas de texto.

Acceso extendido al historial de comandos, permitiendo recuperar y modificar bloques lógicos


en conjunto, realizar búsquedas inteligentes en el historial, etc.

Mensajes de error (traceback) más inteligibles (aunque no dejen de resultar crípticos, sobre todo
al principio).

Multitud de comandos mágicos consistente en comandos cuyo primer carácter es el tanto por
ciento (%), aunque se puede omitir si no hay ambigüedad, y que facilitan mucho el trabajo:

• Ejecución directa de los comandos más usuales del shell desde el propio intérprete. Por ejem-
plo, para ver los archivos en el directorio de trabajo basta con ejecutar %ls o simplemente
ls si no se le ha dado otro valor al nombre ls.
De igual manera, tenemos comandos mágicos como cd, mkdir, etc.
• Evaluación de la duración de ejecución de un comando con la orden %timeit o de un bloque
de código con %%timeit.
• Ejecución de scripts con comandos Python con el comando run. Por ejemplo, run [Link]
no sólo es una alternativa cómoda al estándar exec(open('[Link]').read()), sino que
además tiene muchas opciones que permiten calcular el tiempo CPU consumido, ejecuciones
múltiples, etc.
• Muchos otros que pueden resultar interesantes, pero que conocerlos todos (y documentarlos)
puede representar mucho tiempo.

Acceso rápido a la ayuda disponible de cualquier comando, incluyendo los mágicos, usando el
carácter interrogante ('?'). Un interrogante proporciona la ayuda básica sobre el comando; dos
proporcionan una ayuda extendida (si existe, en caso contrario, escribe lo mismo que uno).

• Un interrogante colocado después del nombre de cualquier objeto proporciona la ayuda


disponible para el tipo o clase del mismo. Por ejemplo, si entero es un objeto del tipo int,
la orden entero? escribirá en pantalla la descripción de la clase int.

Escapes al shell (shell escapes, en inglés), consistentes en el acceso a cualquier programa externo
ejecutable desde el shell usando el carácter exclamación ('!'). La salida puede almacenarse en
una variable con la sintaxis salida = ! comando_del_shell.
Los comandos que tienen versión mágica también pueden ejecutarse como escapes al shell. Sin
embargo, su funcionamiento es muy diferente: al usar comandos mágicos, éstos se ejecutan en el
mismo proceso que ipython, con lo que su efecto siempre será efectivo dentro de él; en un escape
al shell se crea un proceso nuevo (una ejecución del shell) que es donde se ejecuta el comando.
En este último caso, el efecto no tienen por qué ser efectivo en la sesión ipython.
Por ejemplo: la orden ! mkdir directorio crea el directorio directorio, del mismo modo que si lo
creásemos con el comando mágico correspondiente. Sin embargo, la orden ! cd directorio crea
un proceso en el que cambiamos el directorio activo, pero, al acabar el comando, salimos de ese
proceso y regresamos al de ipython, que no ha realizado el cambio.

Además, IPython es el interfaz de comandos usado por algunos IDE más completos, como Jupyter y
Spyder.

6
2.2. ALGUNOS COMANDOS BÁSICOS DE PYTHON.

2.2. Algunos comandos básicos de Python.

Como en el caso de MATLAB, Python proporciona una serie de comandos que vienen incorporados al
lenguaje ( builtins ). A continuación se enumeran alguno de los más básicos, pero que son convenientes
para empezar a usar el lenguaje:

Comentarios: El carácter almohadilla (# ) introduce un comentario que abarca desde su posición


hasta el final de la línea. Además, una cadena de texto por sí sola no tiene ningún
tipo de efecto secundario, por lo que puede usarse como comentario. Este último
tipo de comentario es usado por el intérprete para documentar funciones y clases.
help(a): Muestra en pantalla el mensaje de ayuda de a, que puede ser un comando o un tipo
de datos (help(int)). Por ejemplo, puede usarse para obtener una descripción más
completa del resto de comandos descritos en esta lista.
Conversores de tipo:
Todos los tipos de Python, int, float, list, etc. tienen asociado un comando para
convertir datos a ese tipo. El comando es el propio nombre del tipo usado como
comando; Así, para convertir un número a entero, podemos ejecutar int(3.72).
round(x): Redondea x al entero más próximo. En el caso de equidistancia entre dos enteros,
se toma el entero par más cercano ( redondeo a par o del banquero ), lo cual puede
resultar un poco extraño: round(2.5) == 2, pero round(1.5) == 2, también.
min(x...) y max(x...):
Devuelve el valor mínimo o máximo de su(s) argumento(s). Si se invoca con un único
argumento, éste ha de ser compuesto; si se invoca con varios argumentos, todos ellos
han de ser simples y permitir la comparación numérica.
abs(x): Para números reales, en coma flotante o enteros, devuelve su valor absoluto. Para
números complejos, su módulo.
sum(x...): Devuelve la suma de su(s) argumento(s).
len(x): Devuelve la longitud de la variable de tipo compuesto x; da error si x es de un tipo
simple distinto de la cadena de texto.
print(x...): Muestra en pantalla la representación textual de su(s) argumento(s). Si se invoca
con más de uno, éstos aparecen separados por espacio. En cualquier caso, escribe
una nueva línea al final.
input(s) Escribe el mensaje s en pantalla y espera hasta que el usuario introduce una cadena
de texto usando el teclado. Devuelve la cadena escrita que puede convertirse al tipo
deseado utilizando el constructor correspondiente.
exec(a): Permite ejecutar el bloque de código Python contenido en la cadena de texto a.
eval(a): Devuelve el resultado de evaluar la expresión Python contenida en la cadena de
texto a.

2.3. Operadores de Python.

2.3.1. Operador asignación.

El operador más importante de Python es la asignación, consistente en asignar un nombre a un objeto


usando el signo igual (=):

7
CAPÍTULO 2. MODO INTERACTIVO, COMANDOS BÁSICOS Y EMULACIÓN DE MATLAB.

>>> a = 1
>>> print(a)
1

Este es el mecanismo básico para crear variables en Python (aunque, como se verá más adelante, es
discutible hablar de variables en Python), ya que éstas no se declaran, sino que se definen dándoles
un valor. Es el denominado tipado dinámico: el tipo de la variable se determina en el momento de
definirla, y podemos consultarlo usando el comando type():

>>> a = 1

>>> type(a)
int

>>> a = 1.1

>>> type(a)
float

Al asignar el valor 1 a la variable a, ésta adopta el tipo entero (int). Si, a continuación, le asignamos
el valor 1.1, su tipo pasa a ser real en coma flotante (float). De hecho, lo que hace Python al asignar
el segundo valor es destruir la variable de tipo int y crear una nueva de tipo float.
El nombre de las variables está formado por cualquier cadena formada exclusivamente por subrayado
(_), letras y dígitos, y que no empieza por dígito. En el caso de Python, letra es cualquier carácter
lexicográfico incluido en el estándar UTF-8. Eso incluye, aunque su uso está desaconsejado, letras
como la [ç], la [ñ] y las vocales acentuadas.

2.3.2. Operadores aritméticos.

Los operadores aritméticos incluyen los típicos en otros lenguajes de otros lenguajes de programación:
suma (+), resta (-), multiplicación (*) y división (/). Esta última es la denominada división real y su
resultado es siempre real.
Además de la división real, Python también proporciona la división entera indicada por el operador //.
El resultado de este operador es el truncado a entero del resultado de la división de sus operandos. El
truncado se realiza siempre hacia −∞ (floor()). Si los dos operados son enteros, el resultado también
lo es; en caso de que uno o más de ellos sea real, el resultado es de tipo real, aunque de valor entero:

>>> 4 / 3
1.3333333333333333

>>> 4 // 3
1

>>> -4 // 3
-2

>>> 3 // 4
0

>>> 3. // 4
0.0

8
2.3. OPERADORES DE PYTHON.

El operador % permite calcular el resto de la división entera u operador módulo. Este resto se calcula
de una manera distinta al modo habitual en otros lenguajes como C/C++ o MATLAB: su signo es
igual al del denominador de la división entera:

>>> 3 % 4
3

>>> -3 % 4
1

>>> 3 % -4
-1

>>> -3 % -4
-3

El resto de la división entera se ha diseñado de este modo de manera que el producto de la división
entera por su divisor más el resto siempre sea igual al dividendo (es decir: a partir de la división entera,
el divisor y el resto de la división entera podemos invertir la división entera usando siempre la misma
fórmula):

>>> b * (a // b) + a % b == a
True

Finalmente, el operador ** es el utilizado para la exponenciación. Es importante no confundirlo con


el operador ^, que realiza una operación totalmente distinta: el XOR bit a bit.

>>> 397 ** 461


110261429282586427065790213257480743269614614325765963655088313933664664149451396497864584
501677788566573447515090363659836445068621261140005946217143008159120281853654226144949237
128997480791292993756921186662305269974881980810954257394659420746785014250340166989205783
399477579294419190835088130529999109733971088639222222111382827049848587383513347905121464
487852548658074773911762901211458882951915043931596466863041175977364354349130758494517178
744428313452682753225661306604886126493397554495761258955302946698968766794595352974852363
145181319334818752822996998216825730301508701880042864703238865346656909032108146803585558
459158430847236915961071666359220516774484970275343596723418433236980792921319940504359345
825402881619815519100532327053624779939624760945673150904052060147929885571394528690549987
496812547490345349874171530810352189723280293715702329578913095689028857366024894708787249
325909200844150636580537976041363567899280788947609160122280013306629778608330802450179913
392254280838282216972950148560477897684819927388154625617914607440934027342747680887760179
918874704986564793268577495194921179641695164158767827490496177985603740694146203884012517
27435952536566755907097500797

>>> 397 ^ 461


64

Por cierto, el resultado de 397 ** 461 más arriba tiene 1199 dígitos; (casi) infinitamente más grande
que el número total de partículas en el universo.

2.3.3. Operadores bit a bit.

Python permite realizar operaciones lógicas sobre los bits que forman dos números enteros interpre-
tados como cadenas de bits. Estos operadores son el AND (&), OR (|) y XOR (^):

9
CAPÍTULO 2. MODO INTERACTIVO, COMANDOS BÁSICOS Y EMULACIÓN DE MATLAB.

>>> 6 & 3 # 110 & 011 = 010


2

>>> 6 | 3 # 110 | 011 = 111


7

>>> 6 ^ 3 # 110 ^ 011 = 101


5

También se dispone de la inversión del complemento a 2 del operando (~). Éste es se obtiene inter-
cambiando los 0 y los 1 de la representación en complemento a 2 del número (~ x = -x - 1):

>>> ~ 9
-10

Finalmente, entre las operaciones a nivel de bit también se dispone del desplazamiento de bits, a la
izquierda (<<), equivalente a multiplicar por una potencia de dos, y a la derecha (>>), equivalente a la
división entera por una potencia de dos:

>>> 17 << 3 # 17 * (2 ** 3) = 136


136

>>> 17 >> 3 # 17 / (2 ** 3) = 2
2

2.3.4. Combinación de operador y asignación.

Los operadores aritméticos y de bits permiten su combinación con la asignación anteponiendo el


operador al signo igual, sin espacios entre ellos:

>>> numero = 17

>>> numero <<= 3 # numero = numero << 3

>>> numero # numero = 17 * (2 ** 3) = 136


136

>>> numero += 1 # numero = numero + 1

>>> numero # numero = 136 + 1 = 137


137

2.3.5. Operadores lógicos o booleanos.

El resultado de una operación lógica en Python es un valor booleano que puede evaluar a True o False:

True: Cualquier cosa distinta de False, cero, vacío o nada evalúa como True. Cero es el valor cero
(¿?); más adelante se verá por qué se mencionan también los valores vacío y nada.

False: False, cero y la negación de cualquier cosa que evalúe a True evalúa a False. Vacío y nada
no son iguales a False, aunque evalúan como False; cero sí es igual a False, pero nunca se
debería depender de ello.

10
2.3. OPERADORES DE PYTHON.

Es de suponer que el lector habrá notado la insistencia en usar las expresiones evalúa a o evalúa como,
y la diferenciación que se hace respecto a la más normal es igual a. El motivo es que, en general, no
se debería depender de relaciones de igualdad en las expresiones lógicas. Así, en lugar de if expr ==
True, es en general mejor usar if expr. Del mismo modo, en lugar de if expr == False, suele ser mejor
if not expr.

[Link]. Operadores relacionales.

Son los que comparan dos valores: igualdad (==), diferencia (!=), mayor o igual (>=), etc. Pueden
encadenarse, y el resultado de la cadena se interpreta igual que se haría en una fórmula matemática:

>>> 1 < 4 != 3 == 3
True

>>> 1 < 4 < 3


False

[Link]. Operadores lógicos y cortocircuitos.

Python proporciona los siguientes operadores lógicos sobre valores booleanos: la negación (not), AND
lógico (and) y OR lógico (or).
El resultado de not es siempre el valor booleano True o False. En el caso de and y or, el resultado es
el valor del último operando calculado:

>>> 0 and 13
0

>>> 0 or 13
13

>>> not 0 and not 13


False

>>> 0 or not 13
False

Los operandos involucrados en los operadores and y or sólo se calculan si ello es necesario para de-
terminar el resultado de la operación. Así, si el primer operando de and es falso, ya no es necesario
calcular el segundo para saber que el resultado es falso, y Python no lo hará. Es lo que se denomina
cortocircuito lógico, y que puede ser utilizado para ejecutar condicionalmente el segundo operando:

>>> 0 or print('hola')
hola

>>> 0 and print('hola')


0

>>> 1 or print('hola')
1

>>> 1 and print('hola')


hola

11
CAPÍTULO 2. MODO INTERACTIVO, COMANDOS BÁSICOS Y EMULACIÓN DE MATLAB.

Aunque este comportamiento puede ser útil para realizar la ejecución condicional del segundo ope-
rando, también puede ser peligroso, ya que hay ocasiones en que deseamos que los dos operandos se
ejecuten sí o sí. Por ejemplo, considérese el caso en que se tienen que leer dos valores de un fichero
y realizar algún tipo de comparación. Si después de leer el primero ya se puede conocer el resultado
de la comparación, el segundo no se leerá; pero esto provocará que, la siguiente vez que leamos dos
valores del fichero, el primero de ellos sea el que no se leyó en la comparación anterior.

[Link]. Operadores especiales: in e is.

El operador inclusión (in) comprueba si un valor está contenido en un tipo compuesto. Su negación
se construye con not in:

>>> 1 in {1, 2, 3}
True

>>> 1 not in {1, 2, 3}


False

El operador identidad (is) comprueba si dos objetos son el mismo. Su negación es is not:

>>> 1 is 1
True

>>> 1 is not 1
False

Hay que andar con mucho cuidado con el uso del operador is. Su misión es comprobar si dos objetos
son el mismo, y debería limitarse su uso a este objetivo, que cobrará interés cuando se vean los tipos
mutables. Si lo que se desea es comprobar si dos valores son iguales, el operador que hay que usar es la
igualdad (==). En muchas ocasiones, el uso de la identidad en lugar de la igualdad no dará el resultado
deseado, aunque en ocasiones, y de manera imprevisible y dependiente de la implementación, sí podría
hacerlo.

2.4. Emulación de MATLAB usando Python.

Python permite realizar casi todo lo que se puede hacer con MATLAB (y muchas cosas más). Como en
el caso de MATLAB, buena parte de las funcionalidades no están incorporadas en el lenguaje básico,
sino que están disponibles como bibliotecas externas. En el caso de MATLAB, estas bibliotecas se
denominan cajas de herramientas (toolbox, en inglés) y sus funciones están disponibles con sólo instalar
el correspondiente toolbox. No es extraño que toolbox distintos definan funciones con el mismo nombre,
produciendose colisiones en las que no siempre es inmediato determinar cuál de ellas se va a ejecutar.
En Python, estas bibliotecas se denominan paquetes, que, a su vez, se componen de uno o más módulos.
Todos los paquetes son módulos, aunque no todos los módulos son paquetes. Por ahora, la diferencia
entre unos y otros es irrelevante. Lo que sí es relevante es que los módulos (o paquetes) deben ser
importados explícitamente para poder usarlos; y, en principio, debemos indicar el nombre del módulo
para acceder a los elemento que los componen, indicando módulo y función separados por un punto.
El paquete más importante para emular la funcionalidad de MATLAB es numpy. Este paquete incorpora
la definición del tipo de datos usado por éste y otros muchos paquetes, el [Link], así como buena
parte de las funciones MATLAB básicas, como por ejemplo [Link]() o [Link](). También
incluye módulos con funciones más avanzadas. Por ejemplo, el módulo [Link] incluye la función
[Link]() que calcula la transformada discreta de Fourier de una ndarray.

12
2.4. EMULACIÓN DE MATLAB USANDO PYTHON.

Por ejemplo, podemos generar un vector de 11 elementos entre 0 y 1 usando [Link]() con una
sintaxis idéntica a la de la función linspace() de MATLAB:

>>> import numpy


>>> [Link](0, 1, 11)
array([0. , 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1. ])

La importación de módulos puede realizarse de distintos modos. El más simple es el visto más arriba,
usando la orden import módulo. El problema con este modo de importación es que, en ocasiones, el
nombre a importar es excesivamente largo, y cada vez que queramos acceder a un componente del
paquete tendremos que escribirlo entero.
Para evitar el engorro de los nombres de módulo excesivamente largos existe la alternativa import
módulo as alias. Donde alias es el nombre con el que conoceremos al módulo en nuestro programa.
Los paquetes y módulos más populares tienen alias de uso común que conviene usar. Por ejemplo,
numpy suele abreviarse como np. Nadie nos impide elegir un alias más imaginativo, pero eso puede
desconcertar a cualquier otra persona que lea nuestro código.
Así pues, la generación de la lista de números del ejemplo anterior suele escribirse como:

>>> import numpy as np


>>> [Link](0, 1, 11)
array([0. , 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1. ])

También es posible importar sólo uno o más componentes del módulo o paquete usando la construcción
from módulo import componente. En este caso, el nombre componente deberá usarse directamente, sin
indicar el nombre del módulo:

>>> from numpy import linspace


>>> linspace(0, 1, 11)
array([0. , 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1. ])

2.4.1. Instalación de paquetes usando pip3.

Si ha intentado ejecutar el código de la sección anterior, es muy probable que haya recibido el siguiente
mensaje de error:

>>> import numpy


Traceback (most recent call last):
File "<stdin>", line 1, in <module>
ModuleNotFoundError: No module named 'numpy'

Eso es debido que el paquete numpy no está instalado en su ordenador. Para instalar un paquete de
Python es necesario invocar el programa pip. No obstante, es muy probable que su sistema operativo
ya tenga instalado la versión 2 de Python, con lo que pip podría apuntar al instalador para esa versión.
Para garantizar que el paquete se instala para la versión 3, es conveniente indicarlo explícitamente
usando pip3.
pip3 es semejante a la utilidad apt de Ubuntu. Como en ese caso, tiene comandos específicos para
descargar, instalar, actualizar, desinstalar, etc. paquetes. Puede ver todos los comandos disponibles y
sus opciones ejecutando pip3 help. En el caso de instalar un paquete, la orden es, simplemente, pip3
install paquete:

13
CAPÍTULO 2. MODO INTERACTIVO, COMANDOS BÁSICOS Y EMULACIÓN DE MATLAB.

usuario:~/APAV$ pip3 install numpy


Collecting numpy
Downloading numpy-1.20.1-cp38-cp38-manylinux2010_x86_64.whl (15.4 MB)
| | 15.4 MB 5.0 MB/s
Installing collected packages: numpy
Successfully installed numpy-1.20.1

A pesar de su parecido con la utilidad apt de Ubuntu, pip3 tiene una diferencia importante (y muy
conveniente): salvo que el usuario que realiza la instalación sea el administrador del sistema, el paquete
se instalará en el directorio del usuario, con lo que éste tiene libertad total para instalar paquetes y
para desinstalar aquéllos de los que sea propietario.
Una vez instalado, ya está disponible para su uso desde Python.

2.4.2. Visualización de una señal de audio.

Como ejemplo del empleo de Python como alternativa a MATLAB vamos a poner el problema de leer
y visualizar una señal de audio almacenada en un fichero con formato Microsoft WAVE.
Los paquetes que hemos de instalar son:

soundfile:
También conocido por el alias sf, es un paquete que incorpora funciones de lectura ([Link]())
y escritura ([Link]()) de ficheros de audio de distintos tipos y formatos.
En el ejemplo, usaremos la función [Link]() para leer un fichero de audio. La función
toma como único argumento (por ahora) el nombre del fichero, y devuelve un vector con las
muestras de señal y la frecuencia de muestreo:

>>> muestras, fm = [Link](nombreFichero)

numpy: Alias np, lo importaremos para, usando la función [Link]() generar un vector de tiempos
para los valores del eje de abscisas del gráfico. [Link](número) devuelve un vector con
número enteros desde 0 hasta número - 1.

matplotlib:
Paquete de representación gráfica, incluye diversos módulos. Uno de ellos es [Link],
que incluye funciones análogas a las usadas en MATLAB para este propósito. Por ejemplo,
la función [Link]() es un buen reemplazo del plot() de MATLAB.
El alias de [Link] suele ser plt.
Según el tipo de instalación, la gráfica no se muestra automáticamente al ejecutar la función.
En ese caso, es necesario ejecutar la función [Link]() para verla.

Todo junto, el código para visualizar las muestras el fichero WAVE '[Link]', ejecutaríamos las
órdenes siguientes:

>>> import soundfile as sf


>>> import numpy as np
>>> import [Link] as plt
>>>
>>> x, fm = [Link]('[Link]')
>>> t = [Link](len(x)) / fm
>>> [Link](t, x)
[<[Link].Line2D object at 0x7fd989540f40>]
>>> [Link]()

14
2.4. EMULACIÓN DE MATLAB USANDO PYTHON.

Que produce el resultado siguiente:

Emulación de MATLAB usando Pyhton: visualización de señales y sus perio-


dogramas.
Resuelva los ejercicios indicados en el fichero Anàlisi fitxer de [Link].

15
CAPÍTULO 2. MODO INTERACTIVO, COMANDOS BÁSICOS Y EMULACIÓN DE MATLAB.

16
Capítulo 3

Sistema de tipos de datos en Python.

El sistema de tipos de Python tiene algunas peculiaridades que marcan profundamente el lenguaje.
La primera, la riqueza de su conjunto de tipos básicos. Dominar estos tipos básicos proporciona al
programador herramientas muy potentes que permiten realizar tareas complejas de un modo mucho
más sencillo y elegante que en otros lenguajes.
Un precio, no excesivo, a pagar es que el comportamiento de estos tipos, y aquéllos que cree el propio
programador, difiere en varios aspectos de lo que es habitual. Por ejemplo, todos los objetos que
podamos manejar en Python son miembros de una clase (en el sentido que se le da en programación
orientada a objeto). Y cuando se dice que todos los objetos son miembros de una clase, se está diciendo
que todos los objetos lo son. Eso incluye los tipos proporcionados por el lenguaje y las clases creadas
por el programador, pero también las funciones, los módulos y paquetes, los programas...
Así, todos los operadores vistos en el apartado anterior se corresponden con métodos de una clase. Por
ejemplo, cuando sumamos dos enteros estamos invocando el método int.__add__() de la clase int.

3.1. Objetos, nombres y ¿variables?; el name binding.

Un elemento fundamental de todo lenguaje de programación es su estructura de datos; es decir, el


modo cómo se almacenan y emplean las variables de un programa. Porque, claro, todos los programas
tienen variables, ¿no? Pues no... hay un grupo de lenguajes de programación en los que no tiene sentido
hablar de variables; al menos no del modo como las conocemos en otros lenguajes como C/C++, Java
o incluso MATLAB. Entre estos lenguajes se encuentra Python.
En C/C++, por ejemplo, la declaración de una variable es utilizada para indicar su tipo de datos y
reservar espacio en una posición de memoria para almacenarla. A partir de ese momento podemos (al
menos intentar) proporcionar valores a la variable. Pueden ocurrir tres cosas: si asignamos un valor
del mismo tipo que el declarado, el valor se almacena en la posición de memoria; si el valor es de un
tipo distinto, pero compatible, se realiza promoción de tipos y el valor promocionado al tipo de la
declaración es almacenado; si el valor es de un tipo incompatible con el de la variable, se produce un
error.
En Python, las cosas son algo diferentes: se crean objetos (en principio constantes) que pueden ser
de un tipo u otro y que se almacenan en algún punto de la memoria. A esos objetos les podemos
asignar uno o más nombres para hacer referencia a ellos con posterioridad. Para ello se usa el operador
asignación (=), que es también el mecanismo básico para crear el objeto:

>>> a = 257
>>> a
257

17
CAPÍTULO 3. SISTEMA DE TIPOS DE DATOS EN PYTHON.

Esta operación no almacena el valor 257 en algún sitio vinculado al nombre a, como ocurriría en
C/C++, sino que crea tanto el objeto entero 257 como el nombre a y hace que el segundo quede
vinculado al primero, lo que se conoce como vinculación de nombres ( name binding , en inglés).
Cada objeto tiene una identidad, consistente en un número entero que permite recuperar su valor. Por
ejemplo, en la implementación CPyhton, la identidad es directamente la posición de memoria ocupada
por el objeto. Ésta es una elección bastante lógica, pero no exigida por el lenguaje. Todo lo que éste
exige es que objetos distintos tengan identidades distintas en cada instante (un objeto sí puede tener
la identidad de otro que ya no exista).
Podemos acceder a la identidad del objeto con el comando id():

>>> a = 257
>>> id(a)
140539753721744

La identidad es del objeto, no del nombre. Si realizamos más asignaciones al mismo objeto, todas
comparten la misma identidad:

>>> a = 257
>>> b = a
>>> c = b
>>> print(id(a), id(b), id(c))
140539753275888 140539753275888 140539753275888

También podemos verificar que la identidad es la misma usando el operado is:

>>> a is b is c
True

Tenemos un único objeto, pero al que están vinculados tres nombres distintos. En el lenguaje de los
pythonisos, se dice que el objeto tiene tres referencias. El número de referencias es importante porque,
cuando este número baja hasta cero y ningún nombre está vinculado al objeto, éste se destruye y su
memoria liberada (en pythoniso, reclamada).
Sin embargo, cuando asignamos un objeto nuevo a cualquiera de estos nombres, su identidad cambia.
Esto ocurre aunque el objeto nuevo tenga el mismo valor que el antiguo:

>>> a = 257
>>> id(a)
140539753721744
>>> a = 257
>>> id(a)
140539753721648

Lo que ha ocurrido no tiene nada que ver con lo que pasa con órdenes semejantes en C/C++ o
similares. En Python, al asignar un nuevo valor a un nombre que ya estaba vinculado a un objeto, lo
primero que ocurre es que la vinculación original se destruye, el nombre desaparece y el número de
referencias del objeto se decrementa en uno; a continuación se crea otro objeto con el nuevo valor y
se le vincula un nuevo nombre que, casualmente, coincide con el que se empleaba anteriormente.
Dado que cualquier intento de asignar un valor a un nombre provoca que cualquier vinculación anterior
del mismo nombre se destruya, es complicado decir que el nombre representa una variable. Como
mucho, podría decirse que el nombre representa constantes sucesivas.

18
3.2. TIPOS DE DATOS EN PYTHON.

Más adelante se verá que hay objetos cuyo contenido sí puede variar. Son los miembros de clases
y algunos de los tipos compuestos. Sin embargo, el razonamiento expuesto en este apartado seguirá
siendo válido, pero entendiendo el objeto como el contenedor, que no varía, aunque su contenido sí
pueda hacerlo.

Variables, ¿sí o no?


La afirmación de que en Python no hay variables es un poco radical. La documentación oficial de
Python evita el uso de la palabra variable y da preferencia al uso de nombres o identificadores,
pero nunca menciona que no existan variables en Python.

Hay autores que defienden el punto de vista de que sí hay variables en Python, aunque sean muy
diferentes de las de C/C++. Por ejemplo, en Ned Batchelder puede encontrar una explicación
muy buena de este asunto, en el que el autor acaba defendiendo que sí hay variables en Python
(aunque reconociendo que son raritas).

En estos apuntes se ha decidido adoptar un planteamiento mixto: aunque se insistirá en las


diferencias entre los objetos de Python y las variables de otros lenguajes, se acabará usando este
término para referirnos a ellos, ya que el concepto de variable está muy arraigado y evitar su uso
acaba resultando en expresiones intrincadas y farragosas que más que aclarar los conceptos, lo
que hacen es confundir al lector. En el fondo, lo importante es el comportamiento, no el nombre
que se le dé.

El impropio uso del nombre variable puede ser muy conveniente en muchos casos. Por ejemplo,
si se realiza la instrucción a = 1, decir ”el nombre a está vinculado al objeto 1”, resulta bastante
más farragoso que decir ”la variable a vale 1”.

Sin embargo, es importante tener claro cómo funciona la vinculación de nombres en Python.
Sólo de este modo seremos capaces de predecir el comportamiento de ciertas estructuras que, si
se asume el comportamiento típico de otros lenguajes, resultan paradójicas.

Una vez cambiado el chip y comprendido este modo de funcionamiento, muchas características
a veces oscuras del lenguaje acaban por ser claras como el agua y, sobre todo, de una enorme
simplicidad y coherencia: Python siempre hace lo mismo y del mismo modoa .
a
Una buena analogía es la de los punteros de C. Comprenderlos de verdad puede llevar años de sufrimiento,
pero, cuando se consigue, su uso se vuelve evidente e imprescindible... y los Segmetation Fault y Acces Violation
se acaban convirtiendo en un recuerdo amargo pero lejano.

3.2. Tipos de datos en Python.

Python es un lenguaje de programación fuertemente tipado. Esto quiere decir que una operación sólo
es posible si sus operandos tienen el tipo correcto. En Python, al contrario de otros lenguajes como
C/C++ o Java, no se realiza promoción de tipos. Por ejemplo, << es el operador de desplazamiento de
bits hacia la izquierda. En principio, este operador sólo está definido para valores enteros. Por tanto,
si se intenta aplicar a un valor real, el resultado es un error:

>>> 19 << 3
152

>>> 19.0 << 3


Traceback (most recent call last):

19
CAPÍTULO 3. SISTEMA DE TIPOS DE DATOS EN PYTHON.

File "<stdin>", line 1, in <module>


TypeError: unsupported operand type(s) for <<: 'float' and 'int'

Sin embargo, existen tres características que camuflan de algún modo este carácter fuertemente tipado
del lenguaje:

El tipado dinámico, que consiste que el tipo de un objeto se determina en el momento de su


creación.

• Combinado con la vinculación de nombres, provoca que un mismo nombre pueda ir cam-
biando de tipo de objeto de manera transparente.
• Es como si fuera una variable cuyo tipo de datos no está determinado. Aunque, claro, esto
es más el resultado de interpretar como una variable lo que sólo es un nombre.

La sobrecarga de operadores, con lo cual puede parecer que éstos admiten distintos tipos de
operando, cuando, de hecho, estamos utilizando operandos distintos para cada tipo.

El tipado patoso (duck typing) que consiste en que, aunque el tipo de los datos no sea el que
pensamos, si se le parece lo suficiente, es posible que el operador funcione.

La primera de estas características es una consecuencia inmediata del name binding. En verdad, la
variable no permite objetos de tipos distintos, simplemente es un mismo nombre que apunta a objetos
diferentes. Las otras dos se tratarán en mayor profundidad en la sección dedicada a programación
orientada a objeto.

3.2.1. Tipos simples.

Los tipos simples de Python son aquéllos que permiten almacenar un único valor. Básicamente, son
los números y algunos tipos especiales como None. Las cadenas de texto pueden considerarse como tipo
simple o compuesto, pero, como es posible acceder a cada uno de sus caracteres de manera individual,
se explican en el apartado dedicado a tipos compuestos.

[Link]. Números enteros.

En Python los números enteros tienen resolución infinita. Es decir, se pueden representar números con
tantos dígitos como sea necesario. Esto permite, por ejemplo, manejar sin error los números usados
en sistemas de criptografía basada en clave pública, en los que el número de dígitos supera los 300.
Los números enteros pueden escribirse usando cuatro formas de representación:

Decimal: Un entero decimal es un entero en base 10. Se representa como una cadena de dígitos que
no puede empezar por 0.

1234

Binario: Enteros en base 2, que se representan como cadenas de 0 y 1 empezadas por 0b o 0B.

0b100101

Octal: Enteros en base 8, que se representan como cadenas de dígitos del 0 al 7, empezadas por
0o o 0O.

0o3231750

20
3.2. TIPOS DE DATOS EN PYTHON.

Hexadecimal:
Enteros en base 16, que se representan como cadenas formadas por dígitos y/o las seis
primeras letras del abecedario, en mayúscula o minúscula, y precedidas por 0x o 0X.

0X1Ab2C7

En todos los casos, pueden insertarse tantos caracteres subrayado (_) como se desee para facilitar la
legibilidad. Estos caracteres sólo pueden aparecer de uno en uno y rodeados de dígitos:

0o_1234_5670

Los enteros negativos se representan del mismo modo, pero precedidos del signo menos

[Link]. Números reales.

Los números reales son de resolución finita, que depende de la implementación y la arquitectura del
procesador. En los casos más habituales, Python usa reales de precisión doble (8 bytes), lo que permite
representar valores 0 ≤ |x| ≲ 10308 con una precisión de unos 16 decimales.
Pueden representarse como un número fraccionario, con el punto (.) como separador entre la parte
entera y la fraccionaria. Tanto la parte entera como la fraccionaria pueden ser omitidas, pero no ambas
simultáneamente:

34_709.869

También se pueden representar usando notación científica mediante una mantisa, seguida de la letra
e o E y del exponente:

12.98e-7

[Link]. Números complejos.

Python soporta directamente los números complejos como suma de una parte real y una parte imagi-
naria, que se representa como un número real seguido del carácter j o J:

7.48 + 3.12j

Es obligatorio indicar el valor de la parte imaginaria, aunque ésta sea 1. Así, 3 + j es incorrecto; debe
escribirse 3 + 1j.
Si x es un número complejo, podemos acceder a sus partes real e imaginaria mediante los atributos
real e imag del número:

>>> complejo = 7.48 + 3.12J

>>> [Link]
7.48

>>> [Link]
3.12

Los números complejos implementan el método conjugate(), que proporciona su conjugado:

21
CAPÍTULO 3. SISTEMA DE TIPOS DE DATOS EN PYTHON.

>>> [Link]()
(7.48-3.12j)

El comando abs() calcula el módulo de un número complejo (o el valor absoluto de uno real):

>>> abs(complejo)
8.104615968693397

3.2.2. Tipos compuestos.

En Python existen distintos tipos de datos formados por más de un objeto, son los llamados tipos
compuestos. Estos tipos pueden verse como colecciones, contenedores o cajas, en los que se pueden
almacenar los distintos objetos. Como en cualquier caja, ésta puede estar abierta, en cuyo caso podemos
meter o sacar objetos, o cerrada, en cuyo caso su contenido no se podrá variar una vez creada. En el
caso de que la caja esté abierta hablamos de tipos mutables; si la caja está cerrada, de tipos inmutables.

[Link]. Mutabilidad y hashabilidad.

Como se comentó al explicar la vinculación de nombres en Python, toda asignación crea un objeto
y le vincula un nombre. Volver a asignar otro objeto al mismo nombre provoca que el primer uso
del nombre se destruya y se cree una vinculación nueva. Por tanto, podemos decir que los objetos
asignados son realmente constantes o, dicho en pythoniso, inmutables.
Esto es absolutamente cierto para todos los tipos vistos hasta ahora, los simples, y, en cierto modo,
también lo es en los tipos compuestos: cuando creamos un contenedor éste no puede variar nunca más.
Ahora bien, sí es posible, en alguno de ellos, modificar su contenido; son los tipos mutables.
Un efecto colateral que puede resultar extraño al principio es que la asignación de tipos compuestos
no crea copias de los valores de la colección, sino sólo de la propia colección, que pasa a tener dos
nombres distintos. Si algo cambia en la colección asignada a uno de los nombres, también lo hace en
la colección asignada al otro, porque es la misma colección.

>>> compuesto1 = [13, 2, 27, 19]


>>> compuesto2 = compuesto1
>>> compuesto2[2]
27
>>> compuesto1[2] = 435
>>> compuesto2[2]
435

Mutabilidad e inmutabilidad.

La mayor parte de los tipos compuestos tienen dos versiones: una mutable y otra inmutable. La tabla
siguiente enumera las distintas estructuras soportadas por Python, tanto en su versión mutable como
inmutable, si existen:

Estructura Mutable Inmutable


Cadena de caracteres str
Cadenas de bytes bytearray bytes
Secuencia de objetos list tuple
Conjuntos abelianos set frozenset
Mapas o diccionarios map

22
3.2. TIPOS DE DATOS EN PYTHON.

Puede resultar un tanto chocante la coexistencia de tipos mutables e inmutables con la misma estruc-
tura. ¿Si ya tenemos la versión mutable, para qué podríamos estar interesados en otra que es idéntica
salvo porque sus objetos no pueden variar?
Hay dos tipos de respuesta a esta pregunta:

Por un lado, tenemos cuestiones algorítmicas: el hecho de saber que su valor no va a cambiar
permite a los compiladores aplicar estrategias de optimización más audaces; además, la orga-
nización en memoria de los tipos inmutables es más eficiente que la de los tipos que pueden
variar.
También son útiles en entornos multiproceso o multihilo, porque no nos tenemos que preocupar
de que el valor de un objeto sea modificado por algún otro proceso o hilo externo.

Sin embargo, su utilidad principal es que pueden formar parte de dos tipos de estructura, los
conjuntos (set y frozenset) y los diccionarios (dict), en los que no se permiten los duplicados.
Al construir un objeto de estos tipos, si el elemento fuera mutable, podría variar después de
construido, con lo que podría no respetarse la unicidad de sus elementos.
De hecho, la condición necesaria para formar parte de estas estructuras es que el objeto sea
hashable (ver más abajo). Pero, en principio, sólo pueden serlo los objetos inmutables.

Conviene recordar que, al asignar un nuevo nombre a un objeto preexistente (objeto2 = objeto1), lo
que se asigna es el nombre objeto2 a los datos vinculados a objeto1. El resultado, por tanto, no son dos
objetos distintos iguales, sino dos nombres para el mismo objeto. Si el objeto es mutable y se realiza
algún cambio en los datos vinculados a uno de los dos nombres, los datos vinculados al otro también
varían, porque, en el fondo, son el mismo. Es un comportamiento análogo a los punteros de C/C++:
copiar un puntero no copia los datos apuntados por él, sólo proporciona una referencia nueva a los
mismos datos.

Hashabilidad.

Asociado al concepto de mutabilidad tenemos el de hashabilidad. Un objeto es hashable si tiene asociado


una clave hash que no cambia a lo largo de su vida1 . Su uso principal en Python es el de aliviar la
localización de objetos arbitrariamente complejos en un conjunto de los mismos mediante la búsqueda
en una tabla hash.
Una tabla hash es simplemente una tabla que almacena los objetos junto con sus claves hash:

Clave Objeto
0 349019 objeto_3
1 113602 objeto_0
2 61736 objeto_5
3 957377 objeto_1
4 842521 objeto_4
5 232455 objeto_2

La ventaja del uso de claves hash es que la posición en la tabla de la clave es predecible usando
algoritmos relativamente complejos, en términos algorítmicos, pero extremadamente rápidos.
La idea es la siguiente: supongamos que queremos localizar un objeto en una colección de los mismos. El
procedimiento directo consistiría en comparar el objeto con cada uno de los contenidos en la colección
hasta localizar uno idéntico. La complejidad asociada a esta búsqueda es proporcional al número de
elementos contenidos en la colección y a la complejidad de los propios objetos.
Utilizando una tabla hash, el procedimiento es distinto:
1
De hecho, lo que debe tener el tipo es un método de nombre __hash__() que devuelva el valor de la clave hash.

23
CAPÍTULO 3. SISTEMA DE TIPOS DE DATOS EN PYTHON.

1.- Se calcula una clave, denominada hash, para el objeto a localizar. Esta clave hash es un
número entero que cumple las condiciones siguientes:

Es calculable con un coste computacional reducido.


Es de corta longitud, para evitar sobrecargar en exceso la memoria y para conseguir que
la comparación de claves se realice rápidamente. Habitualmente se toma un entero del
mismo número de bits que la aquitectura del procesador: 32 ó 64 bits.
Cada posible valor de la clave debe tener la misma probabilidad de ser generado.
Todos los valores que evalúen como iguales deben producir la misma clave hash, con
independencia de que su tipo sea igual o no. Por ejemplo, dado que en Python 2 == 2.0,
tanto el entero 2 como el real 2.0 deben generar la misma clave.
• Si valores iguales producen siempre la misma clave, si ésta es distinta, podemos
garantizar que los valores son distintos:
hash(a) ̸= hash(b) ⇒ a ̸= b
• Sin embargo, no es posible garantizar que objetos distintos produzcan claves distin-
tas. De hecho, es imposible, ya que usamos un número finito de bits para representar
objetos de complejidad arbitraria.
a ̸= b ⇏ hash(a) ̸= hash(b)
◦ Cuando dos valores distintos producen la misma clave, decimos que se produce
una colisión.

2.- A partir de la clave se determinan las posiciones posibles del objeto en la tabla.

En principio, a cada clave distinta debería corresponderle una posición distinta.


• Esto es inviable, ya que requeriría que la tabla tuviera un tamaño prohibitivo. Por
ejemplo, en arquitecturas de 64 bits sería necesaria una tabla de casi 1020 posiciones.
• En la práctica se usan algoritmos que permiten trabajar con tablas sólo ligeramente
más grandes que el número de elementos a almacenar. El precio a pagar es que la
clave puede estar alojada en más de una posición de la tabla, aunque todas ellas
son fácilmente predecibles.

3.- Comprobamos si la clave está en alguna de las posiciones posibles. Si se encuentra, se compara
el objeto con el almacenado en la tabla. Si los dos objetos coinciden, la búsqueda finaliza; si
no lo hacen, continuamos a la siguiente posición posible.

4.- Si el objeto no ha aparecido en ninguna de las posiciones posibles de su clave hash, podemos
estar seguros de que la tabla no lo incluía.

La ventaja de esta estrategia es que localizar un objeto en la tabla tiene un coste computacional muy
bajo, del orden del que tiene un acceso directo a la tabla, mucho más bajo que el necesario para
recorrerla comprobando elemento a elemento la coincidencia. La eficiencia computacional depende de
la calidad de las claves hash generadas: cuanto menor sea la probabilidad de colisión, más eficiente es
la búsqueda. En el caso de claves de 64 bits, la probabilidad de colisión es del orden de p ≈ 2−64 , que
podemos considerar despreciable 2 .
En principio, sólo tiene sentido generar claves hash para objetos inmutables. En caso contrario, la clave
hash dejaría de ser válida en cuanto el objeto cambiara de valor. Es más, si el objeto es compuesto,
2
La probabilidad de colisión con claves hash de 64 bits es unas 100.000 veces más pequeña que la de acertar dos
veces seguidas los 6 aciertos de la lotería primitiva. Otra manera de verlo es que, si un programa realiza un millón de
comparaciones por segundo, tardaremos de media medio millón de años en tener una colisión.
A pesar de que la probabilidad de que dos objetos distintos compartan la misma clave hash es bajísima, este hecho
no suele aprovecharse para ahorrarnos la propia comparación de los dos objetos, que debe realizarse igualmente. Sí sirve
para reducir muchísmo el número de comparaciones a realizar, que, en general, se reducirá a sólo una.

24
3.2. TIPOS DE DATOS EN PYTHON.

sólo se puede generar la clave hash si cada uno de sus elementos es, a su vez, hashable. En los tipos
definidos por Python, esto quiere decir que no puede haber rastro de mutabilidad en el objeto.
Sin embargo, en tipos definidos por el usuario (clases), es posible hacer que sean hashables con sólo
definir el método __hash__(). Por lo tanto, es posible tener objetos mutables pero hashables, aunque
sólo en el caso de tipos definidos por el usuario, nunca con los tipos estándar definidos por Python.

[Link]. Cadenas de texto.

En Python, las cadenas de texto son objetos inmutables del tipo str . El modo más inmediato de crear
una cadena de texto es mediante un literal de texto. Un literal es una cadena de caracteres UTF-8
encerrada entre comillas, simples (') o dobles ("). Si el literal comienza por una de ellas, ha de acabar
con la misma. De este modo es posible incorporar comillas de un tipo dentro del literal con sólo usar
como delimitador del mismo el otro tipo:

>>> 'Esto es un "literal"'


'Esto es un "literal"'

>>> "Esto es otro 'literal' distinto"


"Esto es otro 'literal' distinto"

Los literales de texto pueden incorporar las llamadas secuencias de escape, consistentes en una barra
invertida (\) seguida de un carácter. Los más importantes son:

\\ Barra invertida: sirve para incluir una barra invertida sin que se interprete como el comienzo
de una secuencia de escape.
\ space Espacio no separador: sirve para evitar que un espacio divida la cadena en palabras distintas.
\ enter Retorno de carro no separador: sirve para evitar que un retorno de carro divida la cadena en
líneas distintas.
\n Retorno de carro o fin de línea.
\t Inserta un tabulador.
\’ Inserta una comilla simple, evitando que se interprete como final del literal si éste está
encerrado entre comillas simples.
\" Inserta una comilla doble, evitando que se interprete como final del literal si éste está ence-
rrado entre comillas dobles.
\ooo Inserta el carácter cuyo código ASCII en octal es ooo.
\xhhh Inserta el carácter cuyo código ASCII en hexadecimal es hhh.

En ocasiones es deseable evitar que la barra invertida se interprete como inicio de secuencias de escape.
Este es el caso, por ejemplo, al trabajar con rutas de fichero en Windows, o que contienen espacios
precedidos por barra invertida para evitar dividir el nombre en palabras. Esto se consigue precediendo
el literal con la letra r o R (de raw, crudo en inglés):

>>> print('Este literal incluye \n que se interpreta como secuencia de escape')


Este literal incluye
que se interpreta como secuencia de escape

>>> print(r'Este literal incluye \n que no se interpreta como secuencia de escape')


Este literal incluye \n que no se interpreta como secuencia de escape

25
CAPÍTULO 3. SISTEMA DE TIPOS DE DATOS EN PYTHON.

También es posible crear literales de texto que abarquen más de una línea. Esto se consigue encerrando
el texto entre grupos de tres comillas simples o dobles:

>>> '''Este literal


... incluye más
... de una línea'''
'Este literal\nincluye más\nde una línea'

Métodos de la clase str.

Las cadenas de texto soportan un montón de métodos útiles en el tratamiento de textos. Por ejemplo,
el método lower() devuelve una cadena con las mismas letras que la cadena pero en minúsculas; o el
método startswith(s) devuelve True si la cadena comienza con la cadena s:

>>> 'Esta cadena es el ABC de las cadenas'.lower()


'esta cadena es el abc de las cadenas'

>>> 'Esta cadena es el ABC de las cadenas'.startswith('Esta')


True

Se recomienda consultar la documentación de Python 3 para conocer todos los métodos disponibles
para esta clase.

Operadores sobrecargados de la clase str.

Por otro lado, Python también sobrecarga dos operadores básicos: la suma (+) y la multiplicación (*).
El primero permite sumar dos cadenas de texto encadenando su contenido:

>>> 'la' + 'suma' + 'de' + 'cinco' + 'cadenas'


'lasumadecincocadenas'

Del mismo modo, podemos multiplicar una cadena de texto por un número entero, encadenándose el
texto el número de veces indicado:

>>> 'pdqb' * 20
'pdqbpdqbpdqbpdqbpdqbpdqbpdqbpdqbpdqbpdqbpdqbpdqbpdqbpdqbpdqbpdqbpdqbpdqbpdqbpdqb'

Sin embargo, eso no quiere decir que Python haga ninguna promoción de tipos para realizar estas
operaciones, sino que existen métodos específicos para implementar la suma de dos cadenas de texto
y la multiplicación de una cadena por un entero. Si probamos a sumar un número a una cadena, el
intérprete nos avisa del error, confirmando el carácter fuertemente tipado de Python (frente a lenguajes
como C/C++, en los que sí existe promoción de tipos).

>>> 'no le puedo sumar un número a una cadena de texto' + 2


Traceback (most recent call last):
File "<stdin>", line 1, in <module>
----> 1 'no le puedo sumar un número a una cadena de texto' + 2

TypeError: must be str, not int

26
3.2. TIPOS DE DATOS EN PYTHON.

Lo mismo ocurre si intentamos multiplicar dos cadenas de texto:

>>> 'no puedo multiplicar una cadena' * 'por otra'


Traceback (most recent call last):
File "<stdin>", line 1, in <module>
----> 1 'no puedo multiplicar una cadena' * 'por otra'

TypeError: can't multiply sequence by non-int of type 'str'

Otro operador sobrecargado es el resto de la división entera ( %). Este operador se utiliza para emular
las cadenas con formato usadas en las funciones printf() y scanf() de C/C++, aunque su uso se
considera obsoleto:

>>> '%d es un número entero, y %s una cadena de texto' % (9, "'nueve'")


"9 es un número entero, y 'nueve' una cadena de texto"

Veremos en mayor profundidad este operador al estudiar las cadenas con formato en Python. Entonces
se explicarán las opciones más importantes del operador, el método format(), que es la alternativa
clásica para construir cadenas con formato, y las f-strings que es la alternativa moderna y más
interesante de todas, aunque sólo está disponible desde la versión 3.6.

[Link]. Los tipos binarios, bytes y bytearray.

El tipo bytes es muy semejante al str, pero, mientras los objetos del tipo str pueden verse como
tuplas de caracteres (tipo inexistente en Python), los bytes vendrían a ser tuplas de enteros sin signo
de ocho bits.
Pueden introducirse literalmente colocando el carácter b o B antes de la comilla de inicio del literal:

>>> b'Esto es un literal del tipo "bytes"'


b'Esto es un literal del tipo "bytes"'

>>> type(_)
<class 'bytes'>

O usando el constructor de la clase bytes(), usando como argumento un iterable de enteros sin signo
de ocho bits:

>>> bytes((11, 22, 33, 44, 222))


b'\x0b\x16!,\xde'

Nótese que los bytes cuyo código ASCII corresponde a un carácter imprimible se representan con él,
mientras que los que no son imprimibles se representan con su valor hexadecimal.
Por otro lado, los enteros deben ser enteros de ocho bits sin signo. En caso contrario. se produce error:

>>> bytes((256,))
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
----> 1 bytes((256,))

ValueError: bytes must be in range(0, 256)

27
CAPÍTULO 3. SISTEMA DE TIPOS DE DATOS EN PYTHON.

Los tipos bytes y str comparten casi todos los métodos:

>>> b'esto es una cadena de bytes'.title()


b'Esto Es Una Cadena De Bytes'

Pero no son lo mismo y no pueden mezclarse:

>>> b'esto es una cadena de bytes' + 'y esto una cadena de caracteres'
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
----> 1 b'esto es una cadena de bytes' + 'y esto una cadena de caracteres'

TypeError: can't concat str to bytes

Una utilidad de los objetos tipo bytes radica en que son de los pocos tipos de datos en Python que
soportan el llamado protocolo de búfer . Este protocolo establece los mecanismos para intercambiar
datos en formato binario, y es necesario, por ejemplo, para realizar entrada/salida a fichero o para
establecer comunicaciones telemáticas. En esencia, un objeto que sigue el protocolo de búfer es un
objeto en el que sus valores están almacenados en posiciones correlativas de memoria, de manera
semejante a los vectores de C/C++.
En principio, no se usarán para nada, ya que su manejo resulta un tanto farragoso. Sin embargo, en el
Capítulo 8, al tratar la entrada/salida utilizando objetos tipo fichero, estaremos obligados a usarlos.
El tipo bytearray es como el bytes pero mutable en lugar de inmutable. Para construir una variable
de este tipo es necesario invocar al constructor de la clase:

>>> bytearray(b'Esto es un literal del tipo "bytearray"')


bytearray(b'Esto es un literal del tipo "bytearray"')

[Link]. Listas y tuplas.

Una lista ( list ) es un conjunto ordinalizado de elementos de tipo arbitrario. El palabro ordinalizado
se refiere al hecho de que podemos acceder a un elemento concreto de la lista a partir de su posición
en la misma u ordinal. A menudo se utiliza la palabra ordenada para referirse a esta característica de
las listas, pero esto sugiere que los datos contenidos en la lista están ordenados, lo cual no tiene por
qué ser cierto.
Se representa y puede crear a partir de sus elementos separados por coma y encerrados entre corchetes:

>>> lista = [1, 'dos', [3, 4]]

Fijémonos en que esta lista contiene tres elementos: el primero es entero, el segundo una cadena de
texto y el tercero es otra lista de dos elementos enteros.
Podemos acceder a un elemento de la lista escribiendo su índice (con inicio en 0) entre corchetes:

>>> lista[1]
'dos'

>>> lista[2]
[3, 4]

>>> lista[2][0]
3

28
3.2. TIPOS DE DATOS EN PYTHON.

Esta última expresión es remarcable: como lista[2] es, a su vez, una lista, accedemos a su primer
elemento con lista[2][0].
Si el índice es positivo, se empieza a contar desde el inicio; si es negativo, se empieza desde el final,
con el índice -1 apuntando al último elemento de la lista:

>>> lista[-1]
[3, 4]

>>> lista[-3]
1

También es posible acceder a cachos de lista usando lonchas (slices, en ingĺés), que tienen la sintaxis:

primero : postúltimo : incremento

Esta loncha incluye todos los elementos desde el primero, inclusive, hasta el anterior al postúltimo,
tomados de incremento en incremento. Si no se indica el incremento se usa el valor 1, y se puede omitir
también el dos puntos correspondiente. Si no se indica el primero, se toma el primer elemento de la
lista (0). Si no se indica el último, se toma el siguiente al último de la lista, de manera que el último
incluido es el último de la lista (el postultimo será el siguiente al último). De este modo, lista[:]
incluye todos los elementos de lista:

>>> lista[1:]
['dos', [3, 4]]

>>> lista[1: 2]
['dos']

>>> lista[-1:]
[[3, 4]]

>>> lista[-1: : -1]


[[3, 4], 'dos', 1]

>>> lista[-1: 0: -1]


[[3, 4], 'dos']

>>> lista[-1: : -2]


[[3, 4], 1]

>>> lista[-1: 0: -2]


[[3, 4]]

Fijémonos en que lista[-1: : -1] devuelve la lista original ordenada de último a primero (puede
conseguirse lo mismo con el método [Link](), aunque este método modifica la lista in situ,
mientras que la loncha devuelve una nueva lista, respetando la ordenación de la lista original).
Las lonchas son una característica genérica de Python que, además de en las listas, aparece en otras
estructuras de datos secuenciales, como las cadenas de texto:

>>> 'este es un cacho de mi cadena'[11: 16]


'cacho'

29
CAPÍTULO 3. SISTEMA DE TIPOS DE DATOS EN PYTHON.

Métodos y operadores sobrecargados de la clase list.

Como en el caso de las cadenas de texto, Python proporciona un montón de métodos útiles para el
manejo de las listas. En la documentación de Python 3 puede encontrarse la documentación de estos
métodos. En muchos casos, estos métodos son comunes a otros tipos de datos secuenciales como las
cadenas de texto o las tuplas.
Alguno de los métodos más importantes y/o interesantes son:

[Link](x): Añade el elemento x al final de la lista. Es equivalente a lista += [x].

[Link](l): Añade la lista l al final de la lista. Es equivalente a lista += l.

[Link](i): Devuelve el elemento i–ésimo de la lista y lo elimina de la misma. Si se omite


el argumento i, se usa el último.

[Link](i, x): Inserta el elemento x en la i–ésima posición, desplazando los elementos si-
guientes una posición. Nótese que [Link](-1, x) no pondrá x en la
última posición de la lista modificada, sino en la de la original. En la modi-
ficada ocupará la penúltima posición.

[Link](x): Elimina la primera aparición del valor x de la lista. Da un error si x no aparece


en la lista.

[Link](x): Devuelve el número de apariciones de x en la lista.

También existen una serie de comandos, vinculados a métodos de la clase list, que permiten rea-
lizar operaciones habituales con las listas. En general, estos métodos son comunes al resto de tipos
compuestos:

len(lista): Devuelve el número de elementos (longitud, length en inglés) de la lista.

sum(lista): Devuelve la suma de los elementos de la lista. Da error si éstos no se pueden sumar
(por ejemplo, si contienen números y cadenas de texto).

max(lista): Devuelve el valor máximo de la lista. Análogamente, min(lista) devuelve su valor


mínimo.

Acerca de la mutabilidad de las listas.

Como se trata de un tipo mutable, hay que andar con mucho cuidado con las asignaciones, porque lo
que se asigna es la referencia de la lista y no su contenido:

>>> lista = [1, 'dos', [3, 4]]

>>> listaDos = lista

>>> listaDos
[1, 'dos', [3, 4]]

>>> listaDos[1] = 2

>>> listaDos
[1, 2, [3, 4]]

>>> lista
[1, 2, [3, 4]]

30
3.2. TIPOS DE DATOS EN PYTHON.

Fijémonos que al modificar los valores de listaDos, que es una copia de la lista lista, también se
modifican los valores de esta última. Este un caso en el que es útil el comando is; podemos comprobar
que, efectivamente, los dos objetos, lista y listaDos, son realmente el mismo:

>>> listaDos is lista


True

Si lo que queremos es copiar los valores de una lista, y no su referencia, podemos hacerlo usando la
loncha correspondiente a la lista completa, o el constructor de la clase, list(), usando la lista a copiar
como argumento:

>>> listaDos = lista[:] # O, si se prefiere, listaDos = list(lista)

>>> listaDos[0] = 'uno'

>>> listaDos
['uno', 2, [3, 4]]

>>> lista
[1, 2, [3, 4]]

>>> listaDos is lista


False

Un caso típico de error debido a la mutabilidad de las listas y el comportamiento de la asignación de


objetos mutables es la construcción de matrices a partir de la repetición de vectores:
Esto es un error, salvo que se sepa lo que se hace...
>>> m = [[0, 0, 0, 0]] * 4

>>> m
[[0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0]]

Podemos ver que, efectivamente, hemos construido una lista de listas, lo que es análogo a una matriz.
Sin embargo, si damos un valor a un elemento concreto de la matriz, el resultado no es el deseado:

>>> m[1][2] = 2

>>> m
[[0, 0, 2, 0], [0, 0, 2, 0], [0, 0, 2, 0], [0, 0, 2, 0]]

Esto es así porque el comando m = [[0, 0, 0, 0]] * 4 no crea una lista con cuatro listas, sino una
lista con cuatro referencias a la misma lista. Cuando modificamos una de las copias, modificamos las
cuatro.

Tuplas.

Una tupla ( tuple ) es idéntica a una lista, con la salvedad de que se trata de un tipo inmutable. Por
tanto, si se intenta modificar su contenido, el intérprete da error.
Se construye simplemente enumerando sus elementos separados por comas. A menudo es conveniente
rodear los elementos con paréntesis (que sirve para agrupar comandos), aunque no siempre es necesario:

31
CAPÍTULO 3. SISTEMA DE TIPOS DE DATOS EN PYTHON.

>>> 1, 2, 3
(1, 2, 3)

>>> type(_)
<class 'tuple'>

Una coma final es ignorada, al igual que pasa en el caso de las listas y resto de tipos secuenciales, pero
puede ser útil para crear tuplas de un solo elemento, ya que un único elemento rodeado por paréntesis
no construye una tupla3 :

>>> 1, 2, 3
(1, 2, 3)

>>> 1, 2, 3,
(1, 2, 3)

>>> 1,
(1,)

>>> (1,)
(1,)

>>> (1) # Esto no genera una tupla


1

El interés del uso de las tuplas frente al de las listas es triple:

Por un lado, la tupla es más eficiente en términos de almacenamiento, acceso y cálculo. Esto es así
porque las listas están diseñadas internamente de manera que se optimice el tiempo de inserción
de elementos a la misma. Esta optimización de la inserción, sin embargo, repercute negativamente
en la memoria empleada y el tiempo de acceso a sus elementos. Como las tuplas son invariantes,
no es necesario prever la inserción de nuevos elementos y su implementación es más eficiente.

Por otro, las tuplas son hashables si todos sus elementos también lo son, con lo que la comparación
de dos tuplas hashables se beneficia de la comparación de sus claves hash. Sólo será necesario
comprobar elemento a elemento si dos tuplas son iguales cuando sus claves hash lo sean; si no lo
son, ya sabemos de antemano que las dos tuplas serán diferentes.
Por el contrario, como las listas no son hashables, su comparación requiere la comparación de
cada uno de sus elementos.

Finalmente, relacionado con el punto anterior, y el motivo más importante de todos, las tuplas
pueden ser usadas en estructuras de datos que requieren que sus elementos sean hashables, como
los conjuntos o los diccionarios.

Sin embargo, una tupla sí puede contener elementos mutables. En ese caso, la tupla en sí es inmutable,
pero el contenido de estos elementos puede cambiar:

>>> tupla = ([0, 0, 0], [0, 0, 0], [0, 0, 0])

>>> tupla
([0, 0, 0], [0, 0, 0], [0, 0, 0])

>>> tupla[1][1:] = 1,2

3
En este caso, los paréntesis se interpretan como agrupador de comandos, no como constructor de tuplas.

32
3.2. TIPOS DE DATOS EN PYTHON.

>>> tupla
([0, 0, 0], [0, 1, 2], [0, 0, 0])

Si la tupla contiene elementos mutables, no será hashable:

>>> tupla.__hash__()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
----> 1 tupla.__hash__()

TypeError: unhashable type: 'list'

Una característica interesante de las tuplas es que que permiten la asignación múltiple:

>>> a, b, c = 1, 2, 3

>>> a, b, c
(1, 2, 3)

Incluso es posible intercambiar los valores, sin necesidad de utilizar variables auxiliares (como ocurriría
en C/C++):

>>> a, b, c = c, a, b

>>> a, b, c
(3, 1, 2)

De hecho, este intercambio tiene sentido si pensamos en que no se está modificando el valor de ninguna
variable, sino asignando nombres a objetos que permanecen constantes. Los nombres usados en la
parte derecha de la asignación se desvinculan en cuanto se utilizan otros nombres idénticos en la parte
izquierda de la misma.

[Link]. Conjuntos y conjuntos congelados.

Un conjunto ( set ) es una colección mutable y no ordinalizada de elementos hashables no repetidos.


Un conjunto congelado (frozenset) es idéntico a un set salvo que es inmutable y, por tanto, podrá ser
usado en los contextos en los que es necesaria o conveniente la inmutabilidad.
El interés de los conjuntos es doble:

Permiten crear colecciones sin repetición. Al ir añadiendo elementos a un conjunto, el resultado


final estará formado por una única copia de cada uno de ellos.

Los conjuntos permiten aplicar las técnicas propias del álgebra de conjuntos.

Los elementos del conjunto deben ser hashables, es decir, debe ser posible generar una clave hash a
partir de sus valores. En principio, eso sólo es posible si los elementos son inmutables y, en el caso de
ser compuestos, contener únicamente elementos hashables.
Los conjuntos se construyen y representan mediante sus valores separados por comas y encerrados
entre llaves:

33
CAPÍTULO 3. SISTEMA DE TIPOS DE DATOS EN PYTHON.

>>> conjunto = {'blanco', 'rojo', 'azul', 'blanco', 'rojo', 'negro'}

>>> conjunto
{'azul', 'rojo', 'negro', 'blanco'}

Fijémonos en que se han eliminado los duplicados y en que el orden de los elementos en el conjunto
es arbitrario.
Python proporciona los mecanismos básicos del álgebra de conjuntos en forma de métodos de la clase
set y, en muchos casos, como sobrecarga de los operadores básicos:

[Link](x): Añade el elemento x al conjunto. No tiene sobrecarga.

[Link](s): Añade los elementos del conjunto s al conjunto. Se sobrecarga con el


operador |.

[Link](s): Selecciona los elementos del conjunto que también pertenecen a s. Se


sobrecarga con el operador &.

[Link](s): Selecciona los elementos del conjunto que no pertenecen a s. Se sobre-


carga con el operador -.

conjunto.symmetric_difference(s):
Devuelve los elementos del conjunto o de s que sólo aparecen en uno
de los dos. Se sobrecarga con el operador ^.

[Link](s): Devuelve True si todos los elementos de s están contenidos en el con-


junto. Se sobrecarga con el operador <=. Además, el operador < hace
lo mismo pero devuelve False si s es idéntico al conjunto (es decir, es
menor pero no igual).

[Link](): Devuelve un elemento, arbitrario, del conjunto y lo elimina del mismo.


Devuelve error si el conjunto ya está vacío.

[Link](x): Elimina, si está presente, el elemento x del conjunto. El método [Link](x)


hace lo mismo, pero da error si el elemento no está presente en el con-
junto.

Algunos ejemplos de uso de los conjuntos:

>>> conjunto = {'blanco', 'rojo', 'azul', 'blanco', 'rojo', 'negro'}

>>> otro = {'verde', 'amarillo', 'blanco', 'negro', 'rosa'}

>>> [Link](otro)
{'negro', 'blanco'}

>>> conjunto | otro


{'negro', 'rosa', 'verde', 'azul', 'amarillo', 'rojo', 'blanco'}

>>> conjunto < otro


False

>>> (conjunto & otro) < otro


True

>>> conjunto
{'azul', 'rojo', 'negro', 'blanco'}

34
3.2. TIPOS DE DATOS EN PYTHON.

>>> [Link]()
'azul'

>>> conjunto
{'rojo', 'negro', 'blanco'}

Se recomienda consultar la documentación de Python 3 para conocer todas las posibilidades y carac-
terísticas de los conjuntos.

Conjuntos congelados (frozenset).

Los conjuntos exigen que sus elementos sean hashables. Por tanto, no es posible construir conjuntos
de conjuntos:

>>> catalunya = {'rojo', 'amarillo'}

>>> galicia = {'blanco', 'azul'}

>>> euskadi = {'rojo', 'verde', 'blanco'}

>>> banderas = {catalunya, galicia, euskadi}


Traceback (most recent call last):
File "<stdin>", line 1, in <module>
----> 1 banderas = {catalunya, galicia, euskadi}

TypeError: unhashable type: 'set'

Sí podemos realizar conjuntos de frozenset. Éstos se construyen invocando al constructor de la clase,


frozenset(), con el conjunto, u otro iterable (se verá más delante), de sus elementos:

>>> banderas = {frozenset(catalunya), frozenset(galicia), frozenset(euskadi)}

>>> banderas
{frozenset({'rojo', 'amarillo'}), frozenset({'verde', 'rojo', 'blanco'}), ···
,→ frozenset({'azul', 'blanco'})}

Ahora bien, en la mayoría de operaciones, set y frozenset serán indistinguibles, pudiéndose mezclar
sin problema. En el caso de operaciones que involucren a ambos tipos de conjunto, el tipo del resultado
coincide con el del primer operando:

>>> conjunto = {1, 2, 3, 'cuatro'}

>>> congelado = frozenset({2, 3, 4, 'cinco'})

>>> conjunto - congelado


{1, 'cuatro'}

>>> congelado - conjunto


frozenset({'cinco', 4})

>>> conjunto | congelado


{1, 2, 3, 4, 'cinco', 'cuatro'}

>>> congelado | conjunto


frozenset({1, 2, 3, 4, 'cinco', 'cuatro'})

35
CAPÍTULO 3. SISTEMA DE TIPOS DE DATOS EN PYTHON.

[Link]. Diccionarios.

Un diccionario ( dict ) es una estructura de datos semejante a una lista, pero en la que los valores se
direccionan por clave en lugar de por índice. Es análogo a lo que, en otros lenguajes como C++, se
denomina map. Su forma es la de una colección de ítems, en la que cada ítem está formado por una
clave (key) y el valor asociado a la misma (value).
Se representa como una lista separada por comas y encerrada por llaves de los pares clave-valor
separados por dos puntos:

>>> numeros = {0: 'cero', 13: 'trece', 2 / 3: 'dos tercios', 'sesenta y nueve': 69}
>>> numeros
{0: 'cero', 13: 'trece', 0.6666666666666666: 'dos tercios', 'sesenta y nueve': 69}

Accedemos a los valores del diccionario mediante la clave, que tiene que ser idéntica:

>>> numeros['sesenta y nueve']


69

>>> numeros[2 / 3]
'dos tercios'

>>> numeros[0.666]
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
----> 1 numeros[0.666]

KeyError: 0.666

Se pueden añadir ítems al diccionario, o modificar los existentes, mediante asignación:

>>> numeros['siete'] = 7

>>> numeros[13] = 13

>>> numeros
{0: 'cero', 13: 13, 0.6666666666666666: 'dos tercios', 'sesenta y nueve': 69, 'siete': 7}

Los diccionarios, como el resto de tipos, pueden construirse usando el constructor de la clase dict(). En
este caso existen dos posibilidades: a partir de una colección de duplas, en las que el primer elemento
es la clave y el segundo su valor; o a partir de argumentos nombrados por clave (se verá más adelante
qué significa esto).
En el primer caso, a partir de una colección de duplas, la clave puede tener cualquier valor:

>>> numeros = dict(((0, 'cero'), (13, 'trece'), (2 / 3, 'dos tercios'), ('sesenta y ···
,→ nueve', 69)))

>>> numeros
{0: 'cero', 13: 'trece', 0.6666666666666666: 'dos tercios', 'sesenta y nueve': 69}

En el caso de utilizar argumentos por clave para el constructor, las claves deben ser nombres de
identificador válidos en Python, que se almacenarán como cadenas de texto:

36
3.2. TIPOS DE DATOS EN PYTHON.

>>> otros = dict(cero=0, dos_tercios='dos tercios', sesenta_y_nueve=69)

>>> otros
{'cero': 0, 'dos_tercios': 'dos tercios', 'sesenta_y_nueve': 69}

Podemos acceder a los ítems del diccionario con el método items():

>>> [Link]()
dict_items([(0, 'cero'), (13, 'trece'), (0.6666666666666666, 'dos tercios'), ('sesenta y ···
,→ nueve', 69)])

Fijémonos en que, en esencia, items() devuelve una lista de duplas.


También se puede acceder separadamente a las claves, con el método keys(), o a los valores, con
values():

>>> [Link]()
dict_keys([0, 13, 0.6666666666666666, 'sesenta y nueve'])

>>> [Link]()
dict_values(['cero', 'trece', 'dos tercios', 69])

El operador in permite comprobar si el diccionario contiene una entrada para esa clave (pero no para
el valor):

>>> 'sesenta y nueve' in numeros


True

>>> 69 in numeros
False

>>> 2 / 3 not in numeros


False

[Link]. Creación de objetos compuestos vacíos.

A menudo es interesante crear un objeto compuesto vacío. Por ejemplo, para construirlo añadiendo
secuencialmente los elementos que lo forman. El mecanismo necesario para crear el objeto inicial
depende de su tipo. En general, todos los tipos permiten crear un objeto vacío invocando al constructor
del tipo, que tiene el mismo nombre que éste, sin argumentos:

>>> conjunto = set()

>>> type(conjunto)
<class 'set'>

>>> congelado = frozenset()

>>> type(congelado)
<class 'frozenset'>

Alguno de los tipos permiten una notación más compacta usando el delimitador propio del tipo sin
nada dentro. Podemos construir de este modo listas, tuplas y diccionarios:

37
CAPÍTULO 3. SISTEMA DE TIPOS DE DATOS EN PYTHON.

>>> lista = []

>>> type(lista)
<class 'list'>

>>> tupla = ()

>>> type(tupla)
<class 'tuple'>

>>> diccionario = {}

>>> type(diccionario)
<class 'dict'>

Los otros tipos no pueden construirse de este modo, ya que comparten delimitadores con los de éstos.
Así, por ejemplo, el delimitador de tanto los diccionarios como los conjuntos y los conjuntos congelados
es la llave, pero un par de llaves vacías sólo genera los primeros.

[Link]. Iterables e iteradores; el tipo range.

Un tipo es iterable si es posible recorrer sus valores de uno en uno. Para ello, el tipo debe disponer
del método __getitem__() o de los métodos __iter__() y __next__(). Veremos más adelante distintos
mecanismos para construir objetos de este tipo. Por ahora, nos bastará con saber que todos los tipos
compuestos de Python son iterables.
Un tipo iterable permite definir un iterador para recorrerlo. Esto se hace con el comando iter():

>>> conjunto = {'blanco', 'rojo', 'azul', 'blanco', 'rojo', 'negro'}

>>> iterador = iter(conjunto)

>>> type(iterador)
<class 'set_iterator'>

El iterador permite recorrer los valores del iterable al que se refieren usando el comando next().
Éste recorrerá los valores del iterable hasta agotar sus elemento, en cuyo momento producirá el error
StopIteration. Todo iterador es, a su vez, un iterable.
Fijémonos en que, en el ejemplo de los colores, como el iterable es un conjunto, los valores se recorren
sin duplicados y en un orden arbitrario:

>>> next(iterador)
'azul'

>>> next(iterador)
'rojo'

>>> next(iterador)
'negro'

>>> next(iterador)
'blanco'

>>> next(iterador)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
----> 1 next(iterador)

38
3.2. TIPOS DE DATOS EN PYTHON.

StopIteration: 0

Este mecanismo se utiliza en Python para gobernar los bucles for, que se verán cuando se traten las
estructuras de control de flujo.

El tipo range.

Un caso concreto de iterable especialmente útil es el tipo rango ( range ). El tipo rango genera una
secuencia inmutable de números enteros consecutivos, especialmente útil en bucles for al estilo clásico
de C/C++ (for (i = 0; i < N; i++) ...)4 .
Se construye con el constructor de la clase, que puede tomar uno, dos o tres argumentos:

range(final)
range(inicio, final)
range(inicio, final, incremento)

inicio indica el primer entero a generar que, si se omite, se toma 0; final el elemento siguiente al
último; e incremento indica el incremento entre valores consecutivos que, si se omite, toma el valor 1.
El resultado es un objeto que, a casi todos los efectos, se comporta como la lista con los enteros
recorridos por el rango:

>>> rango = range(10)

>>> len(rango)
10

>>> rango[0]
0

>>> rango[-1]
9

Una diferencia entre los rangos y las listas es que, al seleccionar una loncha, el resultado es otro rango:

>>> rango = range(13, 92, 4)

>>> rango[2: 6]
range(21, 37, 4)

La otra diferencia es que range no genera todos los valores del rango, sino que los genera a demanda, es
decir, cada vez que se le pide uno. De este modo, podemos generar rangos de gran tamaño sin ocupar
mucha más memoria que la necesaria para almacenar un único valor.

>>> casiInfinito = range(73 ** 191)

>>> casiInfinito[-4]
78463248206814960828013398559102139850684886048641975384473151004889174844346132956877806113
54745249475632866829372540672184363430379306371432518433655471777589562274528162128819000342
18730021979756795467089305450873377062855472487834182691289415083720345079876428857260566665
71850216283193368167156966539233227628100035248097534647348164687887385634217973

4
Aunque este tipo de bucles se considera poco (o nada) pythónico.

39
CAPÍTULO 3. SISTEMA DE TIPOS DE DATOS EN PYTHON.

NOTA: si no desea ver su a ordenador temblando y sacando humo por todos sus agujeros, no intente
convertir el rango anterior a una lista o semejante: le harían falta varios (muchos) universos enteros
para almacenarla.
Podemos ver qué valores recorrerá el rango convirtiéndolo a una lista o tupla:

>>> list(rango)
[13, 17, 21, 25, 29, 33, 37, 41, 45, 49, 53, 57, 61, 65, 69, 73, 77, 81, 85, 89]

3.2.3. Tipos especiales.

NoneType, Ellipsis, NotImplementedType, etc.


Además de los tipos más o menos convencionales vistos hasta ahora, Python proporciona una serie de
tipos exóticos que sirven para distintos cometidos.
Alguno de estos tipos son los siguientes:

NoneType: Tipo del que sólo hay el elemento None, equivalente al NULL de C/C++ o el Nil de
LISP. Su significado es ninguno o, más habitualmente, nada. Evalúa como False y
suele usarse para indicar alguna situación excepcional. Por ejemplo, en Python todas
las funciones devuelven algo; si no se indica explícitamente el valor de retorno, la
función devuelve None.
Ellipsis: Tipo del que también hay un único elemento, Ellipsis, que también se puede repre-
sentar con tres puntos (...). Este objeto tiene dos usos principales: por un lado, se
utiliza por la biblioteca numpy para indicar un número indeterminado de dimensiones;
por otro, suele usarse como una instrucción que no hace nada, pero que, visualmente,
invita a ser sustituida por algo que sí sea útil (un poco del estilo: ”aquí tengo que
hacer algo, pero lo marco con ... y ya lo haré cuando me vaya bien”). Evalúa como
True.

Exception: Tipo que se usa en la gestión de errores usando excepciones. Hay un montón de ob-
jetos de este tipo predefinidos por Python. Alguno ya se ha visto en los apartados
precedentes: TypeError, KeyError, StopIteration, etc. Puede consultarse la lista com-
pleta en la documentación de Python 3 . Además, el usuario puede definir sus propias
excepciones.
bool: Tipo que contiene los valores True y False usados en las operaciones lógicas.
ndarray: Este tipo no pertenece al conjunto estándar de Python, sino al de la biblioteca numpy.
Sin embargo, será uno de los que más se usarán en algoritmos de procesado de señal.
Semejante a las matrices de MATLAB, se estudiará en detalle junto con numpy.

Esta lista no es exhaustiva, ni mucho menos. Gracias a su facilidad para crear nuevos tipos (en Python
no hay una diferencia marcada entre lo que es un tipo y lo que es una clase), el lenguaje está lleno de
tipos exóticos como éstos. Conforme vayan apareciendo, se explicarán con el detalle necesario.

[Link]. Estructuras y clases.

En Python no existe el concepto de estructuras existente en otros lenguajes como C/C++. Sin em-
bargo, sus funciones pueden ser asumidas por las clases, que se estudiarán en la sección dedicada a la
programación orientada a objeto.
De hecho, en Python todo es un objeto de alguna clase. Eso incluye los tipos simples y compuestos,
las funciones, los paquetes y módulos...

40
3.2. TIPOS DE DATOS EN PYTHON.

[Link]. ndarray de numpy.

Aunque no pertenecen al conjunto de tipos del lenguaje Python, las ndarray juegan un papel funda-
mental en muchas aplicaciones de tratamiento de señales de todo tipo. Así, además de ser la base del
paquete numpy, también se usan en scipy, matplotlib, soundfile, etc., así como en los sistemas de Deep
Learning TensorFlow, PyTorch y otros.
Las ndarray son matrices numéricas n-dimensionales semejantes a las matrices de MATLAB. A dife-
rencia de las list de Python, las ndarray exigen que todos los elementos sean del mismo tipo numérico.
Si una ndarray es creada del tipo entero, si intentamos almacenar un valor real lo truncará, y, si es
complejo, dará un error.
Podemos crear un ndarray invocando la función [Link]() pasándole como argumento un iterable (o
un iterable de iterables, o un iterable de iterables de iterables, o un...) de valores numéricos:

>>> matriz = [Link]([[1, 2, 3], [4, 5, 6]])


>>> matriz
array([[1, 2, 3],
[4, 5, 6]])

El tipo del ndarray será el menos restrictivo de los tipos de los elementos numéricos. Es decir, si uno
es complejo, el ndarray será complejo; si todos son reales, pero hay al menos uno en coma flotante, el
ndarray será real en coma flotante; etc. El orden de preferencia es complex > float > int > bool.

>>> matriz = [Link]([[1, 2, 3], [4, 5.5, 6.3 + 2.7j]])


>>> matriz
array([[1. +0.j , 2. +0.j , 3. +0.j ],
[4. +0.j , 5.5+0.j , 6.3+2.7j]])

Como en el caso de MATLAB, existen funciones para crear matrices de ceros ([Link]()) o de unos
([Link]()). Las dimensiones de las matrices se pasan como único argumento en forma de tupla:

>>> matriz = [Link]((3,2))


>>> matriz
array([[1., 1.],
[1., 1.],
[1., 1.]])

También podemos crear una ndarray indicando el número de dimensiones deseadas, sin proporcionar
valores iniciales, usando la función [Link] y pasándole una tupla con el tamaño de cada dimensión.
La ndarray creada tendrá valores no inicializados; es decir, arbitrarios:

>>> matriz = [Link]((4,2,3))


>>> matriz
array([[[ 1.43570734e-316, 0.00000000e+000, 6.84015879e-183],
[ 8.82058354e-299, 4.45207037e-161, 1.63709432e-260]],

[[-4.66487004e-076, 4.30718156e+117, 9.66303374e+054],


[ 2.22546716e-308, -7.80533465e+019, -7.20278858e+216]],

[[-1.51543382e+222, 2.58193434e-149, 6.23818423e-212],


[ 2.75309383e-303, 3.06706863e-144, 1.01805340e-093]],

[[-8.99755933e+302, -6.81170580e-090, -3.73242727e+101],


[-1.24377691e+207, 1.53667311e-076, 2.07206806e-310]]])

41
CAPÍTULO 3. SISTEMA DE TIPOS DE DATOS EN PYTHON.

El acceso a los elementos de la matriz puede ser a la manera de Python (matriz[1][2]) o a la manera
de MATLAB (matriz[1, 2]). Esta última manera es más eficiente que la primera.
Las ndarrays permiten las operaciones aritméticas típicas entre matrices: suma, multiplicación, etc.
Ahora bien, la multiplicación de dos matrices es la matriz formada por los productos uno a uno de
los elementos de cada una de ellas, que deben ser de la misma dimensión:

>>> a = [Link]([[1, 2, 3], [4, 5, 6]])


>>> b = [Link]([[7, 8, 9], [3, 2, 1]])
>>> a + b
array([[ 8, 10, 12],
[ 7, 7, 7]])
>>> a * b
array([[ 7, 16, 27],
[12, 10, 6]])

Para realizar la multiplicación matricial ha de usarse el operador arroba (@), pero las dimensiones no
pueden estar agrias (deben agree):

>>> a @ b
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
ValueError: matmul: Input operand 1 has a mismatch in its core dimension 0, with gufunc ···
,→ signature (n?,k),(k,m?)->(n?,m?) (size 2 is different from 3)

Sí podemos realizar la multiplicación matricial si transponemos una de las matrices, usando el atributo
T de las matrices:

>>> a.T @ b
array([[19, 16, 13],
[29, 26, 23],
[39, 36, 33]])

>>> a @ b.T
array([[ 50, 10],
[122, 28]])

3.3. Gestión de la memoria; el recolector de basura.

En Python no tenemos que preocuparnos de ubicar la memoria ocupada por un objeto, ésta se ubica
automáticamente al crearnos. Por tanto, no existen funciones semejantes al malloc() de C/C++.
Tampoco suele ser necesario preocuparse de liberar la memoria cuando el objeto deja de ser necesario.
De eso se encarga el recolector de basura ( Garbage Collector , en inglés). Un recolector de basura es
una rutina que se ejecuta periódicamente localizando los objetos que ya no están en uso, liberando la
memoria que ocupan.
En Python, el recolector de basura funciona en base al cómputo de referencias: cada objeto creado tiene
un contador de referencias que se incrementa cada vez que se le vincula un nombre, y se decrementa
cada vez que se elimina una vinculación.
El modo más sencillo de incrementar el cómputo de referencias de un objeto es mediante asignación.
Al crearse el objeto mediante asignación a un nombre, el contador se inicializa a uno. A continuación,
cada vez que asignemos el mismo objeto a otros nombres, el contador se incrementa en uno. También
se incrementa el contador cuando pasamos el objeto como argumento de una función. En este caso,

42
3.3. GESTIÓN DE LA MEMORIA; EL RECOLECTOR DE BASURA.

que no deja de ser una vinculación, el contador se incrementa al entrar en la función y se decrementa
al salir de la misma.
Un objeto decrementa su contador de referencias cuando uno de los nombres a los que está vinculado
deja de ser accesible. El caso más habitual se produce cuando se sale de una función.
El recolector de basura se ejecuta periódicamente localizando objetos cuyo cómputo de referencias es
igual a cero. Cuando encuentra uno, libera su memoria (lo que en Python se conoce como reclamación).
Hay situaciones, sin embargo, en las que es conveniente ordenar la reclamación de la memoria de un
objeto. Por ejemplo, cuando sabemos que un objeto que ocupa una gran cantidad de memoria deja
de ser necesario mientras alguna vinculación sigue estando activa. En estas ocasiones, podemos usar
el comando del, que elimina la vinculación y decrementa en uno el cómputo de referencias:

>>> lista = [1, 2, 3, 4, 5]


>>> lista
[1, 2, 3, 4, 5]
>>> del lista
>>> lista
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
NameError: name 'lista' is not defined

Sin embargo, del no sirve para liberar directa o inmediatamente la memoria ocupada por el objeto:
la memoria sólo se libera cuando el contador de referencias se hace cero, y lo hará cuando se eje-
cute el recolector de basura, algo que, en principio, no está en nuestra mano gobernar (aunque hay
procedimientos para hacerlo).
Por ejemplo, si tenemos dos nombres vinculados al mismo objeto, ejecutar del sobre uno de ellos
eliminará su vinculación, pero el contador de referencias seguirá impidiendo la reclamación del objeto:

>>> lista = [1, 2, 3, 4, 5]


>>> copia = lista
>>> del lista
>>> copia
[1, 2, 3, 4, 5]
>>> del lista
>>> lista
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
NameError: name 'lista' is not defined

También se puede usar el comando del para eliminar elementos o lonchas de un tipo compuesto:

>>> lista = [1, 2, 3, 4, 5]


>>> del lista[1:4]
>>> lista
[1, 5]

43
CAPÍTULO 3. SISTEMA DE TIPOS DE DATOS EN PYTHON.

44
Capítulo 4

Estructuras de control de flujo (y


semejantes).

4.1. Bloques de código.

Las estructuras de control de flujo son la base de la denominada programación estructurada, basada
en el uso de cláusulas específicas para la gestión de la ejecución de los bloques de código (conjunto de
instrucciones que forman una unidad lógica o funcional) que forman un algoritmo. Surge en contra-
posición a la programación no estructurada, más primitiva, en la que esta gestión recae en el propio
programador, generalmente usando instrucciones del tipo GOTO.
Python proporciona los mecanismos clásicos para controlar cómo se ejecutan los bloques de código.
Fundamentalmente, la ejecución condicional o iterativa del bloque. En Python, un bloque de código
siempre viene precedido por una cláusula terminada en dos puntos (:), y puede adoptar dos formas:

Conjunto de instrucciones separadas por punto y coma (;).

Conjunto de líneas de código con un sangrado1 mayor que el de la cláusula que provoca su
creación.

Por ejemplo, y teniendo en cuenta que la cláusula if cond: ejecuta el bloque de código que le sigue
sólo si el valor lógico de cond es True, las estructuras condicionales siguientes son idénticas:

>>> if cond: orden1; orden2; orden3

>>> if cond:
... orden1
... orden2
... orden3
...

La primera de estas formas, con las órdenes en la misma línea que la cláusula, está totalmente desacon-
sejada salvo en los casos en los que las órdenes a ejecutar son realmente sencillas (y, preferentemente,
sólo una).
En los bloques de más de una línea el sangrado marca el inicio y final del bloque. El sangrado es
cualquier combinación de espacios y tabuladores al principio de la línea, aunque se recomienda usar
exclusivamente espacios. Cada vez que se inicia un bloque, el sangrado deba aumentar en una cierta
cantidad de espacios. Lo habitual, y recomendado, es fijar este incremento en cuatro. El bloque finaliza
1
También llamado, incorrectamente, indentado, por traducción cutre del inglés indent.

45
CAPÍTULO 4. ESTRUCTURAS DE CONTROL DE FLUJO (Y SEMEJANTES).

cuando se recupera el nivel de sangrado de la cláusula. En modo interactivo, esto se consigue usando
la tecla de retroceso o dejando una línea completamente en blanco.
Dentro de un bloque podemos tener cláusulas adicionales que introduzcan nuevos bloques. En cada
una de ellas se debe proceder de la misma manera, aumentando el sangrado en la misma cantidad
de espacios. Salvo por la inclusión de otros bloques, todas las líneas de un bloque deben mantener
exactamente el mismo nivel de sangrado:

>>> if cond1:
... orden1
... if cond2:
... orden2
... orden1
...

En caso de no respetarse el mismo nivel de sangrado, el intérprete finalizará su ejecución con la


excepción IndentationError:

>>> if cond1:
... orden1
... if cond2:
... orden2
... orden3
...
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
orden3
^
IndentationError: unindent does not match any outer indentation level

Un bloque no puede estar vacío. Si queremos un bloque que no haga nada, podemos incluir el comando
pass o una elipsis ... (pero, siempre, respetando las reglas del sangrado):

>>> if cond1:
... orden1
... if cond2:
... ...
... orden2
... orden3
...

4.2. Estructuras condicionales.

En Python sólo existe la estructura condicional if ... elif ... else . A pesar de que ha habido
arduas discusiones acerca de la inclusión de una cláusula del tipo switch o case, el BDFL siempre ha
mostrado su firme oposición a la misma ( PEP-3103 ).
Un bloque condicional consiste en una cláusula if, seguida de cero o más cláusulas elif y finalizado
con una cláusula else opcional. La sintaxis es la típica de otros lenguajes de programación, como
C/C++, Java o Bash:

46
4.2. ESTRUCTURAS CONDICIONALES.

if condIf:
bloqueIf
elif condElif1:
bloqueElif1
elif condElif2:
bloqueElif2

...

else:
bloqueElse

Si condIf evalúa a True, se ejecuta bloqueIf y se sale del bloque condicional; si condIf evalúa a False,
se evalúan las distintas condElif en orden; si una de ellas evalúa a True, se ejecuta su bloqueElif
correspondiente y se abandona el bloque condicional sin evaluar el resto de condElif. Si ni condIf ni
ninguna de las condElif evalúa a True, se ejecuta bloqueElse.
Por ejemplo:

>>> a = {'blanco', 'rojo', 'azul'}

>>> b = {'blanco'}

>>> c = {'rosa'}

>>> if b in a and c in a:
... print(a)
... elif b < a | c:
... print(b)
... elif a < b < c:
... print(a, b, c)
... else:
... print(c)
...
{'blanco'}

4.2.1. Evaluación de las condiciones en estructuras condicionales.

Hay que remarcar que, al evaluar las condiciones, ”es igual a” no es lo mismo que ”evalúa como”.
Recordemos la definición de True y False:

False: False, cero, vacío y nada (None) evalúan como False. También evalúa como False la negación
de cualquier cosa que evalúe como True.

True: Cualquier cosa distinta de False, cero, vacío y nada evalúa como True. También lo hace la
negación de cualquier cosa que evalúe como False.

Como se ha explicado anteriormente, nada es el objeto None que aparece, por ejemplo, como valor de
retorno de una función que no devuelve nada explícitamente. Por otro lado, vacío es cualquiera de los
elementos compuestos sin ningún elemento (lista vacía: []; tupla vacía: (); etc.).
Tanto None como cualquiera de los posibles vacíos evalúa como False, pero no son iguales a False. Del
mismo modo, cualquier cosa que no es False, 0, None o vacío evalúa como True, pero no son iguales a
True.
Así que las siguientes expresiones no se van a comportar como podríamos esperar:

47
CAPÍTULO 4. ESTRUCTURAS DE CONTROL DE FLUJO (Y SEMEJANTES).

>>> [] == False
False # 'Vacío' evalúa como False, pero no es igual a False

>>> None == False


False # 'Nada' evalúa como False, pero no es igual a False

>>> [17, 8] == True


False # 'No vacío' evalúa como True, pero no es igual a True

Lo correcto es usar las condiciones directamente, evitando la comparación con True y False (y mucho
menos usar la identidad, is):

>>> if []: print('[] evalúa como False')

>>> if not []: print('not [] evalúa como True')


not [] evalúa como True

>>> if None: print('None evalúa como False')

>>> if not None: print('not None evalúa como True')


not None evalúa como True

>>> if not [17, 8]: print('not [17, 8] evalúa como False')

>>> if [17, 8]: print('[17, 8] evalúa como True')


[17, 8] evalúa como True

La excepción a esto son los valores 0 y 1. En la implementación de referencia de Python,


CPython, el intérprete está implementado de manera que False sí es igual a 0 y True sí es igual
a 1. Pero esto es un detalle de una implementación concreta, no del lenguaje en sí, así que no
se debería usar nunca.

Igualmente, no hay ningún motivo para comparar con ellos; y, desde luego, no es nada pythónico.

El operador ternario ... if ... else ....

Aunque no se puede considerar como una estructura de control de flujo, Python proporciona un me-
canismo adicional para ejecutar código condicionalmente: el operador ternario o expresión condicional .
La sintaxis es:

ordenTrue if cond else ordenFalse

Lo primero que se realiza es comprobar el valor lógico de cond; si éste evalúa a True, entonces se
ejecuta ordenTrue; si evalúa a False, se ejecuta ordenFalse. Tanto ordenTrue como ordenFalse han de
ser expresiones simples (no múltiples expresiones separadas por punto y coma):

>>> resultado = 'es verdad' if not True else 'no es verdad'

>>> print(resultado)
no es verdad

48
4.3. ESTRUCTURAS ITERATIVAS WHILE Y FOR.

Es habitual usar el operador ternario en el interior de expresiones más complejas. Por ejemplo, las
órdenes anteriores pueden escribirse en una sola línea:

>>> print('es verdad' if not True else 'no es verdad')


no es verdad

4.3. Estructuras iterativas while y for.

En Python existen dos estructuras iterativas: el bucle while y el bucle for.

Bucles while.

El bucle while sigue la forma habitual de este tipo de cláusula en otros lenguajes: el bloque de
comandos se ejecutará en tanto la condición evalúe como True:

>>> colores = {'blanco', 'rojo', 'azul', 'verde'}

>>> while colores:


... print([Link]())
...
verde
azul
rojo
blanco

Fijémonos que, en esta expresión, se echa mano de que el conjunto colores evaluará como True en
tanto no esté vacío, y que el método pop() del tipo set devuelve un elemento del conjunto y lo elimina
del mismo (hasta dejar el conjunto vacío y, por tanto, que evalúe como False).

El operador morsa (walrus).


En Python una asignación no tiene valor. Es decir, cuando hacemos color = [Link](),
color toma un valor del conjunto colores, pero la propia asignación no vale nada. Esto impide
realizar cosas habituales en otros lenguajes como while (color = [Link]()) .... En
las últimas versiones de Python se ha resuelto esta carencia introduciendo el operador morsa
(:=) llamado así, en inglés walrus, porque el dígrafo recuerda a los ojos y colmillos de una morsa
(macho)a .

El operador morsa asigna el valor a la variable y, además, adquiere el valor de ésta. Con él, es
posible reescribir el código de más arriba como:

>>> while (color := [Link]()):


... print(color)
...
verde
azul
rojo
blanco

La utilidad del operador radica en que, ahora, disponemos de la variable color para su uso dentro

49
CAPÍTULO 4. ESTRUCTURAS DE CONTROL DE FLUJO (Y SEMEJANTES).

del programa. Desgraciadamente, el operador no está disponible en la versión de referencia para


este curso (Python 3.6).
a
El operador también tiene un nombre más formal, assignment expression, pero nadie lo usa.

Bucles for.

El bucle for es un poco distinto al de C/C++, pero semejante a los de Bash o MATLAB. Su sintaxis
es:

for item in iter:


bloque

Donde iter puede ser tanto un iterable como un iterador, y bloque se ejecuta tantas veces como ítems
contenga, asignando a item el valor de cada uno de ellos:

>>> for color in colores:


... print(color)
...
verde
azul
rojo
blanco

La mecánica interna del bucle for es la siguiente:

1. Si iter es un iterador, lo deja tal cual; si es un iterable (tipo compuesto para el que están
definidos los métodos __iter__() y __next__()), construye un iterador a partir del mismo.
Si no es ninguna de estas dos cosas, Python eleva la excepción TypeError y finaliza la ejecución.

2. Mediante llamadas al comando next(), Python extrae secuencialmente todos sus elementos.

3. Cuando iter ya no tiene más elementos, next() eleva la excepción StopIteration, indicando al
bucle for que debe terminar.

Órdenes continue y break y cláusula else.

Tanto while como for permiten las órdenes, habituales en otros lenguajes de programación, continue
y break :

continue Esta orden da por terminado la iteración actual del bucle y salta a la iteración siguiente.

break Esta orden da por terminado el bucle.

Por ejemplo, el bucle siguiente muestra los enteros entre 0 y 20, excepto los que son múltiplos de 2 o
de 3, y finaliza la ejecución si encuentra un múltiplo de 3 y 5:

>>> for numero in range(1, 20):


... if not numero % 3 and not numero % 5: break
... if not numero % 2 or not numero % 3: continue
... print(numero)
...
1

50
4.3. ESTRUCTURAS ITERATIVAS WHILE Y FOR.

5
7
11
13

La cláusula else en bucles es algo más específico de Python:

else El bloque de código asociado con la cláusula else sólo se ejecuta si el bucle finaliza
normalmente; esto es, porque se ha llegado al final del iterable, en el caso del bucle for,
o porque la condición evalúa a False, en el de while.
Por contra, no se ejecutará si se sale del bucle por una causa distinta a su final natural.
Por ejemplo, por una cláusula break, o porque se sale de la función mediante la orden
return, o porque se produce una excepción.

En el caso anterior, el bucle finalizó por culpa del break producido al encontrar un múltiplo de 3 y 5
(15). Por tanto, no se ejecuta una cláusula else:

>>> for numero in range(1, 20):


... if not numero % 3 and not numero % 5: break
... if not numero % 2 or not numero % 3: continue
... print(numero)
... else:
... print('No se ha encontrado ningún múltiplo de 3 y 5')
...
1
5
7
11
13

Si reducimos el rango a 10, no se produce la condición del break y sí se ejecuta la cláusula else:

>>> for numero in range(1, 10):


... if not numero % 3 and not numero % 5: break
... if not numero % 2 or not numero % 3: continue
... print(numero)
... else:
... print('No se ha encontrado ningún múltiplo de 3 y 5')
...
1
5
7
No se ha encontrado ningún múltiplo de 3 y 5

Uso de for con contadores.

Cuando el iterable es ordinalizable (en principio, cadenas de texto, listas y tuplas), se corre el riesgo
de, tal vez llevados por la nostalgia, querer usa un contador. Aunque en el campo del procesado de
señal esto puede ser incluso deseable, en general ha de considerarse una solución poco pythónica.
Para usar for con un contador, usamos la función range(), que devuelve un iterador de enteros, y
len(), que devuelve el número de elementos del iterable.

51
CAPÍTULO 4. ESTRUCTURAS DE CONTROL DE FLUJO (Y SEMEJANTES).

Correcto, pero muy poco pythónico


>>> numeros = [13, 7, 23, 49]

>>> for i in range(len(numeros)):


... print(i, numeros[i])
...
0 13
1 7
2 23
3 49

Incluso cuando el valor del índice i es necesario dentro del bucle, un pythoniso de verdad evitaría el
uso de un contador de esta manera. Entre otras cosas, porque esto sólo se puede hacer con tipos ordi-
nalizables, pero no con conjuntos o diccionarios. Un pythoniso auténtico usaría la función enumerate(),
que, por cada elemento del iterable, devuelve una dupla con el contador y el elemento extraído:
Así es como lo hacen los pythonisos de verdad
>>> for i, numero in enumerate(numeros):
... print(i, numero)
...
0 13
1 7
2 23
3 49

Mezcla de iterables con zip().

Hay ocasiones en que lo que interesa es mezclar datos alineados contenidos en iterables distintos. Por
ejemplo, podemos tener una lista con los nombres de un grupo de alumnos, otra con sus apellidos y
otra con sus notas:

>>> nombres = 'Blanca', 'Carles', 'David'

>>> apellidos = 'Agirrebarrenetse', 'Balcells', 'Garcia'

>>> notas = 9, 4.99, 7

Ahora queremos ligar cada nombre con su apellido y su nota. Una opción, nada pythónica, sería
utilizar un contador para recorrer las tres tuplas al mismo tiempo.
La solución pythónica es usar el comando zip() . El comando zip(iter...) tiene por argumentos una
serie de iterables o iteradores y devuelve un iterador de tuplas formadas por un elemento de cada uno
de ellos:

>>> alumnos = zip(nombres, apellidos, notas)

>>> alumnos
<zip object at 0x7fe057544b88>

>>> next(alumnos)
('Blanca', 'Agirrebarrenetse', 9)

>>> next(alumnos)
('Carles', 'Balcells', 4.99)

>>> next(alumnos)
('David', 'Garcia', 7)

52
4.4. COMPRENSIONES (COMPREHENSIONS).

>>> next(alumnos)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration

Habitualmente, zip() se usa en bucles for:

>>> for nombre, apellido, nota in zip(nombres, apellidos, notas):


... print(nombre, apellido, nota)
...
Blanca Agirrebarrenetse 9
Carles Balcells 4.99
David Garcia 7

Hay que tener cuidado cuando los distintos iterables no son de la misma longitud: zip() finalizará
cuando se agote el más corto de ellos. Eso quiere decir que habrá valores que serán descartados. Si
estos valores son importantes, puede usarse la función zip_longest() disponible en el módulo itertools ,
donde se puede encontrar un buen montón de utilidades para construir iteradores e iterables de todo
tipo (permutaciones, particiones, aleatorizaciones, etc.).
zip() puede usarse para realizar muchas cosas útiles, aparte de su empleo en bucles:

>>> unos = (1, 'uno', 'I')

>>> doses = (2, 'dos', 'II')

>>> treses = (3, 'tres', 'III')

>>> cifras, palabras, romanos = list(zip(unos, doses, treses))

>>> cifras
(1, 2, 3)

>>> palabras
('uno', 'dos', 'tres')

>>> romanos
('I', 'II', 'III')

4.4. Comprensiones (comprehensions).

Una manera habitual de construir un objeto de tipo compuesto es partir de un objeto vacío e irle
añadiendo los elementos conforme están disponibles:

>>> mediosPares = []

>>> for numero in range(10):


... if not numero % 2:
... mediosPares += [numero // 2]
...

>>> mediosPares
[0, 1, 2, 3, 4]

53
CAPÍTULO 4. ESTRUCTURAS DE CONTROL DE FLUJO (Y SEMEJANTES).

Una alternativa, mucho más eficiente (y pythónica), es el uso de comprensiones (del inglés com-
prehension, aunque cuesta comprender el nombre). Las comprensiones son estructuras típicas de la
programación funcional2 (explicada el Capítulo 7), cuyo objetivo es, justamente, evitar el uso de bucles,
proscritos en programación funcional y muy ineficientes en lenguajes interpretados como Python.
En el caso de las listas, hablamos de list comprehensions , y su sintaxis, adaptada de Haskell3 , es:

lista = [expr for item in iterable if cond]

Esta orden construye una lista con los valores de expr calculados para cada uno de los item del iterable
que cumplen la condición cond, que es opcional. Es equivalente al bucle:

lista = []
for item in iterable:
if cond:
[Link](expr)

Así, la construcción de las listas del ejemplo anterior se puede escribir como:

>>> mediosPares = [item // 2 for item in range(10) if not item % 2]

>>> mediosPares
[0, 1, 2, 3, 4]

Las comprensiones pueden anidarse tanto como se desee. Por ejemplo, podemos construir una lista
con pares de números pares no repetidos con el bucle anidado siguiente:

>>> paresPares = []

>>> for uno in range(5):


... if not uno % 2:
... for dos in range(5):
... if not dos % 2 and uno != dos:
... [Link]((uno, dos))
...

>>> paresPares
[(0, 2), (0, 4), (2, 0), (2, 4), (4, 0), (4, 2)]

Usando comprensión de lista, el mismo resultado se obtiene, de manera más compacta y eficiente
computacionalmente, con:

>>> [(uno, dos) for uno in range(5) if not uno % 2


... for dos in range(5) if not dos % 2 and uno != dos]
[(0, 2), (0, 4), (2, 0), (2, 4), (4, 0), (4, 2)]

Del mismo modo, pueden construirse conjuntos (set comprehensions) con sólo sustituir los corchetes
por llaves; y, si además sustituimos expr por una entrada de diccionario, exprKey: exprVal, se obtiene
un diccionario (dictionary comprehension):

2
Aunque forman parte importante de los conceptos de la programación funcional, su uso generalizado en Python,
incluyendo los otros paradigmas de programación más convencionales, hace que resulte conveniente su presentación entre
las estructuras de control de flujo, aunque, en el fondo, no tengan mucho que ver con ellas.
3
Haskell es el lenguaje de programación funcional más extendido. Casualmente, en su página de inicio se muestra un
ejemplo de list comprehension.

54
4.5. OTRAS ESTRUCTURAS DE CONTROL.

>>> {'dígito ' + str(item) : item for item in range(4)}


{'dígito 0': 0, 'dígito 1': 1, 'dígito 2': 2, 'dígito 3': 3}

También es posible generar iteradores encerrando la comprensión entre paréntesis (generator com-
prehension). En este caso, dado que el resultado es un iterador, accedemos secuencialmente a sus
elementos con el comando next():

>>> impares = (numero for numero in range(10) if numero % 2)

>>> impares
<generator object <genexpr> at 0x7fc01c618410>

>>> next(impares)
1

>>> next(impares)
3

4.5. Otras estructuras de control: funciones, clases y las cláusulas


try y with.

Aunque no siempre encuadradas dentro de lo que se denomina control de flujo, Python proporciona
cuatro mecanismos adicionales que permiten controlar cómo se ejecutan bloques de código:

def Usada para definir funciones. Se estudiará en la sección dedicada a la programación pro-
cedimental.

class Usada para definir clases. Se estudiará en la sección de programación orientada a objeto.

try Usada para gestionar las excepciones. Se verá en detalle al explicar el manejo de errores
en la programación procedimental.

with Usada para definir gestores de contexto. Se estudiará al explicar una de sus aplicaciones
principales, la entrada/salida por fichero.

55
CAPÍTULO 4. ESTRUCTURAS DE CONTROL DE FLUJO (Y SEMEJANTES).

56
Capítulo 5

Programación procedimental y
definición de funciones.

La programación procedimental es el paradigma más habitual en programación imperativa, es decir,


aquélla en la que el programador diseña las acciones concretas que debe realizar el programa para
alcanzar el resultado.
Es un tipo de programación estructurada (es decir, basada en las estructuras de control de flujo
vistas en el capítulo anterior), en la cual se divide el problema en tareas más sencillas, cada una de
las cuales se pueden programar, depurar y optimizar de manera independiente. Estas unidades más
sencillas reciben el nombre de subrutinas, procedimientos o, más habitualmente, aunque de manera
menos precisa, funciones.

5.1. Subrutinas (funciones).


En Python, una función es un bloque de código como los vistos en la Sección 4.1 y encabezado por
una cláusula def:

def función(argumentos):

En principio, argumentos es una lista de argumentos separados por coma y que se interpretan posicio-
nalmente, esto es: la invocación debe incluir tantos argumentos como la definición de la función, que
se asocian uno a uno según su posición. Éste es el modo estándar de paso de argumentos la mayor
parte de lenguajes de programación. Más adelante, en la Sección 5.5, se verán otros mecanismos muy
potentes para el paso de argumentos.
En Python, todas las funciones devuelven algo a la función desde la que han sido invocadas. Esto se
puede hacer explícitamente usando el comando return. En caso de no usarse el comando return, la
función devuelve el objeto None al finalizar su ejecución.
Una cadena de texto no asignada al principio del bloque de una función es utilizada como documen-
tación de la función y puede ser accedida, junto a información genérica acerca del prototipo de la
función usando el comando help(funcion).
Por ejemplo, la función siguiente devuelve la división entera de sus argumentos, en caso de que ésta
sea entera, y el objeto None si no lo son:

>>> def divideSiDivisible(nume, deno):


... """
... Si nume es divisible por deno, devuelve la división
... entera. Si no lo es, devuelve None.
... """

57
CAPÍTULO 5. PROGRAMACIÓN PROCEDIMENTAL Y DEFINICIÓN DE FUNCIONES.

... if not nume % deno:


... return nume // deno

Una vez definida, la función está disponible para ser usada. Por ejemplo, podemos ver el mensaje de
ayuda:

>>> help(divideSiDivisible)
Help on function divideSiDivisible in module __main__:

divideSiDivisible(nume, deno)
Si nume es divisible por deno, devuelve la división
entera. Si no lo es, devuelve None.

También podemos ver el resultado ejecutando la función con argumentos concretos:

>>> print(divideSiDivisible(12, 4))


3

>>> print(divideSiDivisible(12.0, 4))


3.0

>>> print(divideSiDivisible(12, 5))


None

Si necesitamos devolver más de un valor, podemos agruparlos en forma de tupla. El resultado puede
ser recibido como tupla, o como sus elementos por separado:

>>> def cocienteModulo(nume, deno):


... """
... Devuelve el conciente entero y el resto de
... la división entera (mod) de dos números.
... """
... return nume // deno, nume % deno
...

>>> resultado = cocienteModulo(12, 5)

>>> print(resultado)
(2, 2)

>>> cociente, modulo = cocienteModulo(12, 5)

>>> print(cociente, modulo)


2 2

Funciones almacenadas en ficheros.

Lo habitual es que funciones tan útiles como las vistas en la sección precedente se almacenen en un
fichero de manera que se evite el tener que escribirlas cada vez que se desea disponer de ellas.
Usando nuestro editor de texto preferido1 , escribimos la definición de las dos funciones en el fichero
[Link]:
1
El que esto escribe tiene predilección por vim (Vi IMproved) , pero no se le ocurriría recomendarlo a nadie. Opciones
mucho más recomendables son Notepad++ o el Visual Studio Code de Microsoft. Lo que sin duda no es muy recomendable
es usar el editor de texto por defecto del sistema operativo (aunque, en sus últimas versiones, incorporan mejoras que
resuelven algunas de sus carencias).

58
5.2. CONVENIO DE NOMENCLATURA DE LOS IDENTIFICADORES.

[Link]
1 """
2 Biblioteca con definiciones importantes para la división de números.
3 Se incluyen las funciones divideSiDivisible() y cocienteModulo().
4 """
5
6 def divideSiDivisible(nume, deno):
7 """
8 Si nume es divisible por deno, devuelve la división
9 entera. Si no lo es, devuelve None.
10 """
11
12 if not nume % deno:
13 return nume // deno
14
15
16 def cocienteModulo(nume, deno):
17 """
18 Devuelve el conciente entero y el resto de
19 la división entera (mod) de dos números.
20 """
21
22 return nume // deno, nume % deno

Para cargar estas definiciones en Python hemos de ejecutar el fichero. Esto lo podemos hacer ejecutando
el comando exec() con el texto contenido en el fichero:

>>> exec(open('[Link]').read())

El comando run de ipython3.


El intérprete ipython3 proporciona toda una serie de comandos mágicos que van más allá del
estándar. Uno de ellos es el comando %run (que también puede ejecutarse como run, a secas).
Este comando permite ejecutar un fichero escrito en Python desde dentro del intérprete, pero
como si fuera invocado desde el sistema operativo:

>>> run [Link] # o, también, run divisiones, sin extensión

El comando tiene opciones muy interesantes, que permiten cosas como pasar argumentos en
línea de comandos o calcular el tiempo de ejecución. Hay que tener en cuenta, no obstante, que
ésta es una extensión de ipython3, no presente en el estándar.

5.2. Convenio de nomenclatura de los identificadores.

La PEP 8 incluye la Guía de Estilo de Python . En esencia, es una colección de reglas que debe seguir un
programa Python para que los pythonisos lo consideren pythónico. Vale la pena leerlo porque incluye
muchos consejos útiles que ayudan a hacer más legibles y mejor mantenibles los códigos fuente que
los siguen.
También incluyen auténticas chorradas. Por ejemplo, Guido & co. exigen que todo el código y su
documentación estén escritos en inglés ortográficamente correcto (aunque el propio Guido no es inglés,
sino holandés). Seguir estos consejos, tanto los útiles como las chorradas, es opcional. Sin embargo, si
no se siguen, IDEs como PyCharm, que incluyen un corrector sintáctico, llenarán la pantalla de avisos
inútiles que camuflarán los que son realmente útiles.

59
CAPÍTULO 5. PROGRAMACIÓN PROCEDIMENTAL Y DEFINICIÓN DE FUNCIONES.

Una de las recomendaciones más útiles es la referida a la nomenclatura de los identificadores ; es decir,
los nombres asignados a constantes, variables, funciones, clases, métodos... En lo fundamental, se
recomienda usar palabras completas (o abreviaciones muy pero que muy claras para todo el mundo).
Hay dos maneras de unir las palabras de un nombre para formar un identificador válido: mediante el
uso del subrayado (_) como separador (esto_es_una_variable), o mediante la alternancia de minúsculas
y mayúsculas (el denominado CamelCase porque recuerda a las jorobas de un camello).
Para las variables estándar, la guía de estilo recomienda el uso de palabras en minúscula separadas por
subrayado. No obstante, el uso de subrayados incrementa la longitud de los identificadores y dificulta
su escritura, sin que ello redunde en una mayor legibilidad. Por este motivo, en muchas otras guías de
estilo se prefiere el estilo CamelCase, que también se recomendará en este curso junto al preferido en
el Pythoniverso.
Teniendo en cuenta esta predilección por el CamelCase, estos apuntes siguen y recomiendan las reglas
siguientes:

Constantes:
Las constantes se escriben enteramente en mayúsculas separadas por subrayado. Por ejemplo,
el número de puntos de una señal puede ser NUMERO_PUNTOS.
Se pueden usar abreviaturas cuando, por su contexto, se esté seguro de que su significado
queda suficientemente claro (puede usarse NUM_PNT, pero no NP).
En algunos casos, pueden usarse abreviaturas de uso extendido (NFFT).

Variables genéricas y funciones:


Los nombres de variables genéricas y funciones se pueden escribir usando el estilo camelCase
o con palabras en minúscula separadas por subrayado2 . En cualquier caso, el identificador
debe empezar con minúscula.
Por ejemplo, y usando el criterio camelCase, podemos usar numeroPuntos o numPnt. Usando
palabras en minúscula, tendríamos numero_puntos o num_pnt.

Conflictos:
Cuando la elección obvia para un identificador entra en conflicto con una palabra reservada
del lenguaje, se añade un subrayado al final del nombre. Por ejemplo, si el nombre obvio para
una variable es list, usaríamos el nombre list_.

Ficheros:
Los nombres de fichero usarán siempre minúsculas separadas por subrayado para evitar pro-
blemas con los distintos sistemas operativos.

Clases: Los nombres de clases usan el estilo CamelCase; esto es, empezando cada palabra, incluyendo
la primera, con mayúscula: VectorReal. También podría insertarse un subrayado separando
las palabras, aunque resulta redundante (Vector_Real) y, como dice PEP-8, horrendo.

Atributos y métodos de clases:


El tratamiento de los nombres de atributo y método en las clases es algo especial. Como se
verá en el Capítulo 6, en Python no hay elementos privados, todos son públicos (a fin de
cuentas, somos todos adultos, ¿no?). Sí se puede indicar la preferencia del programador por
que nadie meta la mano en alguno de ellos. Esto se hace con el uso de subrayados al inicio
del identificador3 :
2
Aunque, en general, se recomienda mantener la fidelidad a uno de los dos estilos, evitando su mezcla.
3
En la lista siguiente se hace referencia a un supuesto carácter público o privado de los elementos. Se ha de insistir
en que, en verdad, todo es público. Lo que se indica en la lista es el ánimo del programador.

60
5.3. PRUEBAS UNITARIAS.

Elementos publicos:
Se usa el mismo criterio que para los identificadores de variables genéricas y funcio-
nes. Es decir, según el estándar camelCase con la primera palabra en minúscula, o
usando palabras en minúscula separadas por subrayado.
Elementos privados:
Cuando el programador quiere indicar que nadie debería acceder directamente a un
elemento, su nombre debe empezar por un único subrayado.
Elementos muy privados:
Cuando el programador quiere indicar que un elemento es privado, pero que mucho
mucho, su nombre debe empezar por dos subrayados y no acabar con dos o más.
Los nombres de elementos de clases comenzados por dos subrayados son sometidos
a mangoneo por parte del intérprete (name mangling). El mangoneo consiste en
modificar su nombre precediéndolo por el nombre de la clase y un subrayado:

clase.__elemento → clase._clase__elemento
objeto.__elemento → objeto._clase__elemento

Fijémonos que esto no evita que se pueda acceder al elemento, sólo lo dificulta.

Nombres especiales:
En Python existe toda una serie de elementos especiales. En su mayoría son variables, funcio-
nes, métodos o atributos que es el propio Python quien se encarga de gestionar y/o que tienen
un significado especial para él. Los identificadores asociados a estos elementos empiezan y
acaban con dos subrayados.
Por ejemplo, al incluir un literal de texto al inicio de una función, éste se interpreta como
la documentación de la función, de manera que es accesible con el comando help(). Interna-
mente, Python almacena el literal de texto en el atributo __doc__4 .

5.3. Pruebas unitarias.

Un aspecto fundamental en programación es el de arbitrar mecanismos para comprobar el correcto


funcionamiento de las unidades de código creadas. Son las denominadas pruebas unitarias . Este tipo
de pruebas permite localizar problemas en las etapas iniciales de un proyecto de software, incluyendo
errores sintácticos, de funcionamiento o carencias en la definición de las especificaciones. A menudo,
el propio diseño de las pruebas unitarias permite mejorar el diseño del elemento bajo prueba.
La mecánica de las pruebas unitarias consiste en ejecutar el elemento de software en una serie de
condiciones predeterminadas y comprobar si el resultado obtenido es el esperado. El conjunto de con-
diciones probadas debería incluir los resultados esperados en condiciones normales de funcionamiento,
así como los que se deben esperar en situaciones anómalas.
Por ejemplo, es muy probable que un proyecto tenga que usar una función como divideSiDivisible(),
vista en la sección 5.1:

>>> def divideSiDivisible(nume, deno):


... """
... Si nume es divisible por deno, devuelve la división
... entera. Si no lo es, devuelve None.
... """
... if nume % deno == 0:
... return nume // deno

4
Sí, en Python todos los objetos, incluidas las funciones, pertenecen a una clase y, por tanto, tienen atributos y
métodos asociados.

61
CAPÍTULO 5. PROGRAMACIÓN PROCEDIMENTAL Y DEFINICIÓN DE FUNCIONES.

Es necesario, por tanto, garantizar que la función va a dar el resultado correcto en cualquier condición.
Podemos plantear un conjunto de pruebas unitarias usando distintas combinaciones de los argumentos
nume y deno cuyo resultado conocemos de antemano:

nume deno divideSiDivisible(nume, deno)


4 2 2
5 2 None
0.666 0.333 2.0

Diseñaríamos las pruebas comprobando que la función devuelve los resultados esperados para estos
argumentos. Por ejemplo, para la primera de ellas tendríamos algo del estilo siguiente:

>>> if divideSiDivisible(4, 2) != 2:
... raise Exception('Error en divideSiDivisible(4, 2) != 2')
... else:
... print('Prueba correcta: divideSiDivisible(4, 2) == 2')
...
Prueba correcta: divideSiDivisible(4, 2) == 2

Evidentemente, es imposible predecir en todos los casos todas las posibles situaciones. Por ejemplo,
ninguna de las pruebas contempla la posibilidad de que nume = 0. Al ejecutar la función en este caso,
el resultado es un desastre:

>>> divideSiDivisible(17, 0)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 2, in divideSiDivisible
ZeroDivisionError: integer division or modulo by zero

Una situación de este tipo provocó el desastre en el lanzamiento inaugural del cohete espacial Ariane 5 .
Algo que, se hubiera podido evitar con un conjunto de pruebas unitarias (y también de las denominadas
pruebas de integración ) mejor diseñadas.
En el caso de divideSiDivisible(), las pruebas unitarias deberían haber incluido alguna con el deno-
minador igual a cero:

nume deno divideSiDivisible(nume, deno)


... ... ...
17 0 None
... ... ...

Lo cual hubiera llevado a un mucho mejor diseño de la función:

>>> def divideSiDivisible(nume, deno):


... """
... Si nume es divisible por deno, devuelve la división
... entera. Si no lo es, devuelve None.
... """
... if deno == 0: return None
... elif nume % deno == 0:
... return nume // deno

El diseño de pruebas unitarias siguiendo el procedimiento descrito más arriba presenta varias limita-
ciones, más allá del hecho de que nunca seremos capaces de prever todas las situaciones posibles:

62
5.3. PRUEBAS UNITARIAS.

Las pruebas se ejecutan siempre, con lo que se puede penalizar muy seriamente las prestaciones
del programa.

Al incluir las pruebas en el propio código, éste queda enmarañado de tal manera que su segui-
miento y mantenimiento se ven muy afectados.

Se carece de un esquema de pruebas unificado, cada una debe diseñarse de manera separada y
la información proporcionada es diferente en cada caso.

Los mensajes que nos informan de la superación con éxito de las pruebas aparecen cada vez que
se ejecutan, con lo que obtenemos líneas y líneas de información que, una vez sabemos que la
función se comporta correctamente, son superfluas.

• La alternativa de que las pruebas no escriban nada cuando se superan con éxito tampoco
es deseable, ya que entonces carecemos de información de si se han realizado o no...

5.3.1. Pruebas unitarias en Python; la biblioteca doctest.

La definición estándar de Python proporciona dos mecanismos para implementar pruebas unitarias: las
bibliotecas unittest y doctest . Además, fuera del propio lenguaje, otros desarrolladores proporcionan
aún más.
unittest permite automatizar la realización de multitud de comprobaciones, pero es relativamente
complicada de usar y requiere la escritura de bastante código fuente, lo que resulta en que el código
de comprobación puede acabar escondiendo el código realmente funcional. Por su parte, doctest está
mucho más limitada en sus prestaciones, pero su funcionamiento es muy sencillo y no se afecta en
absoluto al código fuente. Por estos dos motivos, en este curso sólo se usará esta última.
La idea de doctest es incluir las pruebas unitarias como parte de las cadenas de documentación de
las funciones, __doc__. De este modo, al ejecutar la función, las pruebas se omiten completamente.
Una función definida en la biblioteca, [Link](), se encarga de analizar todas las cadenas de
documentación presentes en el módulo y ejecutar las pruebas contenidas en ellas.
Cada prueba consiste en la orden que se quiere ejecutar seguida de la salida en pantalla que tal prueba
produciría en el caso de ejecutarse interactivamente. Puede ejecutarse de este modo cualquier orden
válida de Python, usando las definiciones existentes en el momento de invocar a la función. Cada
una de estas órdenes debe ir precedida del prompt habitual en el intérprete interactivo de Python,
esto es: tres signos mayor que (>>>). Por su parte, la salida debe indicarse de manera textual idéntica
a la esperada por parte del intérprete (aunque hay excepciones explicadas en mayor detalle en la
documentación de doctest , pòr ejemplo para el tratamiento de las excepciones o de las direcciones de
memoria).
Por ejemplo, la cadena de documentación de la función divideSiDivisible podría ser:

>>> def divideSiDivisible(nume, deno):


... """
... Si nume es divisible por deno, devuelve la división
... entera. Si no lo es, devuelve None.
...
... >>> divideSiDivisible(4, 2)
... 2
... >>> divideSiDivisible(5, 2)
...
... >>> divideSiDivisible(0.666, 0.333)
... 2.0
... >>> divideSiDivisible(17, 0)
...
... """

63
CAPÍTULO 5. PROGRAMACIÓN PROCEDIMENTAL Y DEFINICIÓN DE FUNCIONES.

... if deno == 0: return None


... elif nume % deno == 0: return nume // deno

Nótese que la definición de la función no ha variado en absoluto, sólo su documentación (que, de


alguna manera, se ha visto enriquecida de cara al posible usuario).
Para ejecutar los tests, debemos importar doctest y ejecutar la función [Link]():

>>> import doctest


>>> [Link]()
TestResults(failed=0, attempted=4)

En una sesión interactiva, la función nos informa de que se han realizado 4 pruebas, todas ellas con
éxito. Si incluyéramos la función en un fichero y ejecutáramos éste como un script, la salida sería
completamente silenciosa.
En el caso de incluir algún test que no tenga éxito, la función nos informa de lo ocurrido. Por ejemplo,
si imponemos que la salida a divideSiDivisible(0.666, 0.333) sea 3.0, el resultado será mucho más
elocuente:

>>> [Link]()
**********************************************************************
File "__main__", line 10, in __main__.divideSiDivisible
Failed example:
divideSiDivisible(0.666, 0.333)
Expected:
3.0
Got:
2.0
**********************************************************************
1 items had failures:
1 of 4 in __main__.divideSiDivisible
***Test Failed*** 1 failures.
TestResults(failed=1, attempted=4)

En el caso de que todas las pruebas sean exitosas, la salida de [Link]() es muy escueta.
Especialmente si ejecutamos el fichero como un script, y no desde el propio intérprete. Podemos
obtener más información de las pruebas realizadas con éxito usando el argumento verbose=True (o
usando la opción -v si ejecutamos el fichero como un script):

>>> [Link](verbose=True)
Trying:
divideSiDivisible(4, 2)
Expecting:
2
ok
Trying:
divideSiDivisible(5, 2)
Expecting nothing
ok
Trying:
divideSiDivisible(0.666, 0.333)
Expecting:
2.0
ok
Trying:
divideSiDivisible(17, 0)

64
5.3. PRUEBAS UNITARIAS.

Expecting nothing
ok
1 items had no tests:
__main__
1 items passed all tests:
4 tests in __main__.divideSiDivisible
4 tests in 2 items.
4 passed and 0 failed.
Test passed.
TestResults(failed=0, attempted=4)

Ejercicio: manejo de números primos.


Escriba el fichero [Link] que incluya las funciones siguientes:

esPrimo(numero): Devuelve True si su argumento numero es primo, y False si no lo es.


primos(numero): Devuelve una tupla con todos los números primos menores que numero.
descompon(numero): Devuelve una tupla con la descomposición en factores primos de numero.

Usando estas tres funciones, escriba otras dos que calculen el máximo común divisor y el mínimo
común múltiplo de sus argumentos.

mcd(*numeros):
Devuelve el máximo común divisor de sus argumentos.

mcm(*numeros):
Devuelve el mínimo común múltiplo de sus argumentos.

Estas dos últimas funciones deben tener las características siguientes:

Aunque se trate de una solución sub-óptima, en ambos casos deberá partirse de la descom-
posición en factores primos de los argumentos usando las funciones esPrimo(), primos()
y/o descompon().

Aunque se trate de una solución sub-óptima, ninguna de estas dos funciones puede depen-
der de la otra. Es decir, no se puede usar el resultado de mcd() para obtener el de mcm().

Inicialmente se puede considerar que sólo se pasan dos argumentos; por ejemplo, def
mcm(numero1, numero2).

• En la solución final deberá contemplarse un número arbitrario de argumentos; por


tanto, la definición de la función será del tipo def mcm(*numeros).

El fichero deberá empezar con una cadena de texto de documentación que incluya el nombre y
apellidos del autor.

Las cinco funciones deberán contar con su correspondiente cadena de texto de documentación.

Se valorará lo pythónico de la solución; en concreto, su claridad y sencillez. También se valorará


su eficiencia computacional.

65
CAPÍTULO 5. PROGRAMACIÓN PROCEDIMENTAL Y DEFINICIÓN DE FUNCIONES.

5.4. Manejo de errores y excepciones.

En Python, cuando se produce un error durante la ejecución, ésta se interrumpe, indicando la causa
del error en la pantalla (el ratro, o traceback, del error). Esto es radicalmente distinto a otros lenguajes
como el C, en los que es posible ir encadenando errores de ejecución sin ninguna queja... hasta que el
programa salta por los aires con un Segmentation Fault o Access Violation, probablemente en un punto
alejado de la fuente del error, y proporcionando un mensaje completamente inútil para el usuario.
El mecanismo seguido por Python para indicar la aparición de un error es la excepción. Cuando se
produce un error, Python interrumpe la ejecución de la función en la que se encuentra y eleva una
excepción a la función que la invocó. A su vez, la función invocadora interrumpe la ejecución y eleva
la excepción a la función que la invocó a ella. Y así sucesivamente hasta que se llega al nivel superior
y se interrumpe la ejecución del programa.
Ahora bien, el mecanismo de elevación de excepciones está ligado con el de su captura, consistente en
que la función en la que que se produce la excepción, o alguna de las funciones que la reciben, puede
enterarse de la situación de error y manejarla de algún modo.

EAFP vs. LBYL.


El manejo de las situaciones de error mediante elevación de excepciones y su posible captura, es
típico en lenguajes como Prolog, Ruby, Java, etc. y también se ha extendido a otros como C++
o MATLAB. En Python es el mecanismo recomendado, y responde a la filosofía denominada
EAFP : easier to ask for forgiveness than permission. En EAFP se evita comprobar las condi-
ciones que pueden hacer que un comando falle antes de ejecutarlo. En vez de ello, se ejecuta el
comando y, si falla, se producirá una excepción que, en principio, finalizará la ejecución.

Este manejo contrasta con el más habitual en lenguajes como C en el que, antes de ejecutar un
comando susceptible de producir error, se comprueban las condiciones que podrían provocarlo.
Es el denominado LBYL : look before you leap. El problema con LBYL es doble: por un lado,
puede que no consideremos todas las posibles situaciones de error; por otro, puede que las
condiciones de error cambien entre el momento de comprobarlas y el de ejecutar el comando.

Por ejemplo, en C, y siguiendo LBYL, podríamos comprobar si un fichero existe antes de abrirlo:

if (access(fichero, F_OK)) {
fpIn = fopen(fichero, "r");
}

En este ejemplo podríamos tener dos tipos de problemas:

1. Puede que el fichero exista, pero que el usuario no pueda abrirlo; por ejemplo, porque no
tenga permiso de lectura sobre él.

2. Puede que, entre que se hace la comprobación de existencia y el momento de la apertura


se haya producido una situación que impida su apertura; por ejemplo, que otro proceso
haya borrado el fichero.

Un buen programador de C no lo haría así...


El ejemplo puesto más arriba es un ejemplo clásico de los pythónicos defensores de EAFP. No
obstante, un buen programador de C haría algo intermedio entre EAFP y LBYL:

if ((fpIn = fopen(fichero, "r")) == 0) {


fprintf(stderr, "Error al abrir el fichero %s\n", fichero);
return -1;
}

66
5.4. MANEJO DE ERRORES Y EXCEPCIONES.

Tal vez, el motivo por el que el pythoniverso tenga preferencia por EAFP esté relacionado con
la ausencia del operador morsa (:=) en las versiones de Python anteriores a la 3.9.

Eso sí, un no tan buen programador podría abrir el fichero sin comprobar si se ha producido
el error. El resultado podría ser un completo desastre. Por el contrario, EAFP garantiza que
ningún error se queda sin tratar.

5.4.1. Excepciones y rastro (traceback).

En Python, EAFP implica que, cuando se detecta una situación de error, el programa no se limita a
comunicar este hecho y continuar la ejecución (como se haría en LBYL), sino que eleva una excepción
y, en principio, finaliza la ejecución. Como indica el décimo principio del Zen del Python :

Errors should never pass silently.

Antes de finalizar el programa, no obstante, Python escribe en pantalla un informe completo de dónde
y por qué se ha producido el error. Es el traceback o rastro. El problema de los tracebacks, que ya han
ido apareciendo en estos apuntes, es que pueden llenar la pantalla de mensajes un tanto crípticos que,
más que ayudar al usuario poco experimentado, lo llenan de frustración.
Para ver un ejemplo de traceback complejo, vamos a escribir dos ficheros con diversas funciones:
[Link] y esto_peta.py (el ejemplo es más completo si los scripts se importan, en lugar de ejecu-
tarse con el comando run):
[Link]
1 def divide(x, y):
2 return x / y

esto_peta.py
1 from divide import divide
2
3 def peta1(x, y):
4 return divide(x, y)
5
6 def peta2(x, y):
7 return peta1(x, y)
8
9 def peta3(x, y):
10 return peta2(x, y)

Importamos el fichero esto_peta.py y comprobamos que funciona correctamente:

>>> from esto_peta import peta3

>>> peta3(2, 3)
0.6666666666666666

Si provocamos una excepción intentando dividir por cero, el resultado es:

>>> peta3(2, 0)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "/home/albino/APAV/albino/esto_peta.py", line 10, in peta3
return peta2(x, y)

67
CAPÍTULO 5. PROGRAMACIÓN PROCEDIMENTAL Y DEFINICIÓN DE FUNCIONES.

File "/home/albino/APAV/albino/esto_peta.py", line 7, in peta2


return peta1(x, y)
File "/home/albino/APAV/albino/esto_peta.py", line 4, in peta1
return divide(x, y)
File "/home/albino/APAV/albino/[Link]", line 2, in divide
return x / y
ZeroDivisionError: division by zero

Lo que ha ocurrido es que, al intentar dividir por cero en la función divide(), Python ha elevado una
excepción del tipo ZeroDivisionError. Esta excepción pasa a la función que invocó divide(), peta1()
que, a su vez, se la pasó a peta2() y ésta a peta3(). Finalmente, la excepción pasa al módulo que
invocó peta3(), que no es otro que la consola de Python.
Vemos que el traceback nos narra la historia del error, empezando por la función que nosotros hemos
invocando (peta3()) y acabando en la función que efectivamente ha provocado el error (divide()). En
cada nivel de función, se nos informa de la función que se ha encontrado la excepción, el fichero y la
línea en la que se realiza su invocación, y el comando que ha elevado la excepción al siguiente nivel.
Generalmente, las últimas líneas son útiles para saber cuál es el tipo de error; y las primeras lo son
para saber quién lo provocó. Hay que irse acostumbrando a interpretar estos rastros porque, incluso
cuando se tiene experiencia con ellos, puede ser complicado extraer información útil de los mismos.
Sirva de incentivo para intentarlo el que, aunque escondida y críptica, algo de información sí es posible
encontrar (tampoco piense el lector que el traceback es algún tipo de panacea que permite localizar y
solucionar todos los problemas de nuestros programas; por mucha experiencia que se tenga con ellos).

5.4.2. Elevación de excepciones.

El programador puede elevar excepciones a voluntad mediante el comando raise . La utilidad de estas
excepciones controladas es que podemos proporcionar información mucho más precisa y/o útil del
error producido.
La sintaxis de raise es:

raise [excepción [from contexto]]

Ejecutado sin argumentos, raise provoca un RuntimeError que interrumpe la ejecución del programa.
Si se indica el argumento excepción, éste debe ser una instancia de la clase BaseException . Esta clase
incluye múltiples excepciones típicas, como IndexError o KeyError. También incluye la subclase genérica
Exception que nos permite personalizar el tipo de mensaje mostrado en el traceback:

>>> raise Exception('Error al leer el fichero')


Traceback (most recent call last):
File "<stdin>", line 1, in <module>
----> 1 raise Exception('Error al leer el fichero')

Exception: Error al leer el fichero

El argumento opcional from contexto permite manejar el contenido del traceback. En concreto, si
contexto es None, toda la información del estado de la pila se omite en el mensaje de la excepción.

5.4.3. Captura de excepciones.

Ahora bien, el Zen de Python, aparte de decir que los errores no deben pasar silenciosamente, también
dice, a continuación:

68
5.4. MANEJO DE ERRORES Y EXCEPCIONES.

Unless explicitly silenced.

Es decir, hay un mecanismo para poder manejar el error a nuestro antojo, la captura de excepciones,
consistente en la cláusula try: ... except: ... else: ... finally: ... . Ésta permite controlar qué
se hace si se produce una excepción durante la ejecución de un bloque de código.
La sintaxis es algo complicada, así que se va a enunciar los aspectos más relevantes de manera incre-
mental.
En su formato más sencillo, try sólo incorpora una o más cláusulas except:

try:
bloqueTry
except exc1:
bloqueExc1
except exc2:
bloqueExc2
except:
bloqueExc

Al encontrar esta cláusula, Python ejecuta el bloqueTry y, si no se produce ninguna excepción durante
su ejecución, continúa el programa a partir del final de la cláusula completa. Si, durante la ejecución de
bloqueTry se produce una excepción, la ejecución salta al bloque correspondiente a la cláusula except
de esa excepción, si la encuentra, o al correspondiente a la cláusula except anónima (sin excepción
explícita), si ésta existe y no ha encontrado la excepción concreta en ninguna de las cláusulas except
precedentes.
Si se ha encontrado la excepción concreta producida, o existía una cláusula except anónima, se ejecuta
el bloque de código correspondiente, la excepción se borra y se continúa la ejecución como si nada
hubiera pasado:

>>> try:
... 1 / 0
... except ZeroDivisionError:
... print('Intentas dividir por cero')
...
Intentas dividir por cero

Puede haber tantas cláusulas except explícitas como se quiera, pero sólo una, o ninguna, cláusula
except anónima:

>>> try:
... 1 / 0
... except IOError:
... print('Error de entrada/salida')
... except MemoryError:
... print('Error de memoria')
... except:
... print('Algo ha pasado, pero no sé lo que es')
...
Algo ha pasado, pero no sé lo que es

La cláusula else se ejecuta siempre que no se haya producido una excepción al ejecutar el bloqueTry,
a no ser que se finalizara la ejecución de éste por una cláusula return, break o continue. Sólo puede
haber una cláusula else en cada bloque try:

69
CAPÍTULO 5. PROGRAMACIÓN PROCEDIMENTAL Y DEFINICIÓN DE FUNCIONES.

>>> try:
... 1 / 2
... except ZeroDivisionError:
... print('Intentas dividir por cero')
... else:
... print('No ha habido ninguna excepción')
...
No ha habido ninguna excepción

La cláusula finally se ejecuta siempre, pase lo que pase en bloqueTry o en las cláusulas except o else
precedentes.

>>> try:
... 1 / 0
... except ZeroDivisionError:
... print('Intentas dividir por cero')
... else:
... print('No ha habido ninguna excepción')
... finally:
... print('===> Acabo el bloque try...except...else...finally')
...
Intentas dividir por cero
===> Acabo el bloque try...except...else...finally

>>> try:
... 1 / 2
... except ZeroDivisionError:
... print('Intentas dividir por cero')
... else:
... print('No ha habido ninguna excepción')
... finally:
... print('===> Acabo el bloque try...except...else...finally')
...
No ha habido ninguna excepción
===> Acabo el bloque try...except...else...finally

5.4.4. El comando assert.

El comando assert proporciona un mecanismo para depurar programas sin sobrecargar la CPU durante
la ejecución.
Existe una discusión eterna entre los defensores del uso de programas de depuración (debuggers) y
sus detractores. Un debugger es una herramienta fantástica para la depuración de programas: permite
la ejecución paso a paso del programa, la colocación de puntos de parada (breakpoints), visualizar el
valor de las variables, el estado de la pila, etc.
Sin embargo, numerosos programadores de primer nivel, como Linus Torvalds, Brian W. Kernighan,
Rob Pike, o el muy benevolente dictador vitalicio de Python, Guido van Rossum, afirman usar poco
o nada los debuggers, o, directamente, despotrican de su uso5 .
Una de las alternativas más usadas por los detractores de los debuggers es el uso de chivatos. Un
chivato es un bloque de código que comprueba una condición y, si se cumple, eleva una excepción,
mostrando en pantalla un mensaje explicativo.
Un ejemplo de chivato sería:
5
El que esto escribe, sin ser un programador del nivel de los mencionados, tampoco usa apenas debuggers.

70
5.5. PASO DE ARGUMENTOS A FUNCIONES.

if not condición:
raise AssertionError(mensaje_explicativo)

El problema de los chivatos es que roban tiempo de cálculo. Esto, durante el desarrollo del programa
no suele ser un problema; pero puede resultar inaceptable en producción.
El comando assert proporciona un mecanismo que permite colocar chivatos que sólo se activan durante
la fase de desarrollo del programa, desactivándose al compilarlo para distribución. Su sintaxis es:

assert condición, mensaje

Que es equivalente al código siguiente:

if __debug__:
if not condición raise AssertionError(mensaje_explicativo)

La ventaja es que __debug__ es una constante mágica de Python. Su valor por defecto es True, pero, si
se le indica al compilador que optimice el código generado con la opción -O, su valor es False. Como
ese es un valor conocido en tiempo de ejecución, y se ha especificado que se optimice el código, Python
simplemente omitirá las cláusulas assert que encuentre.
Una ventaja adicional de assert es que, al elevar la excepción, Python escribirá en su traceback la línea
que ha provocado el error, con lo que obtendremos información bastante completa de qué problema
ha habido:

>>> assert None == False, 'None evalúa a False, pero no es igual a False'
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
assert None == False, 'None evalúa a False, pero no es igual a False'

AssertionError: None evalúa a False, pero no es igual a False

5.5. Paso de argumentos a funciones.

El paso de argumentos a funciones en Python presenta algunas particularidades que lo diferencian


de otros lenguajes de programación. Estas particularidades se producen tanto en la definición de las
funciones como en el momento de su invocación, utilizando estructuras paralelas, aunque con significado
diferente. Dado que los conceptos aparecen entrelazados, se irá explicando la sintaxis de definición e
invocación alternadamente, rematando la sección una recapitulación comparativa de ambos.
Antes de entrar en materia, conviene tener presente que, en Python, los argumentos se evalúan antes
de efectuarse la llamada a la función en estricto orden de izquierda a derecha . Así, la orden siguiente
aporta un valor distinto a cada argumento de la función print():

>>> lista = [1, 2, 3, 4, 5]

>>> print([Link](0), [Link](0), [Link](0), lista)


1 2 3 [4, 5]

71
CAPÍTULO 5. PROGRAMACIÓN PROCEDIMENTAL Y DEFINICIÓN DE FUNCIONES.

5.5.1. Argumentos inicializados y no inicializados en la definición.

El modo más inmediato de definir una función es con el uso de argumentos no inicializados, de manera
análoga a la definición estándar de funciones en otros lenguajes de programación como C/C++, Java
o MATLAB:

1 def funcion(uno, dos, tres):


2 print(uno, dos, tres)

Al invocar la función deberemos proporcionar tres y sólo tres valores que se asignarán de izquierda a
derecha a los tres argumentos de la función.

>>> funcion(1, 2) # Falta un argumento


Traceback (most recent call last):
File "<stdin>", line 1, in <module>
----> 1 funcion(1, 2)

TypeError: funcion() missing 1 required positional argument: 'tres'

>>> funcion(1, 2, 3) # Esto funciona correctamente


1 2 3

>>> funcion(1, 2, 3, 4) # Sobra un argumento


Traceback (most recent call last):
File "<stdin>", line 1, in <module>
----> 1 funcion(1, 2, 3, 4)

TypeError: funcion() takes 3 positional arguments but 4 were given

Python también permite el uso de argumentos inicializados . Estos argumentos tienen un valor por
defecto que, en caso de no explicitarse al invocar la función, se asignan al argumento correspondiente.
El valor por defecto se indica a continuación del nombre del argumento, separado de éste mediante un
signo igual (=)6 :

1 def funcion(uno, dos, tres=3):


2 print(uno, dos, tres)

Esta función puede invocarse con tres argumentos o dos. En este segundo caso, el argumento tres
toma el valor por defecto:

>>> funcion('uno', 'dos', 'tres')


uno dos tres

>>> funcion('uno', 'dos')


uno dos 3

Los argumentos inicializados deben aparecer después del último argumento posicional. Esto es así para
evitar ambigüedades en la asignación. No hacerlo da lugar a error de sintaxis:

6
Según la guía de estilo de Python ( PEP - 8 ), recomienda no colocar espacios en blanco alrededor del signo = al
inicializar argumentos de funciones.

72
5.5. PASO DE ARGUMENTOS A FUNCIONES.

>>> def funcion(uno, dos=2, tres):


... print(uno, dos, tres)
...
File "<ipython-input-165-b4fde0ace96f>", line 1
def funcion(uno, dos=2, tres):
^
SyntaxError: non-default argument follows default argument

5.5.2. Argumentos posicionales y por clave en la invocación.

Los argumentos posicionales son los encontrados en la mayor parte de lenguajes en los que la asignación
de los valores de la invocación se realiza uno a uno con los parámetros de la función. Es la mecánica
de asignación que se ha utilizado en los ejemplos precedentes.
Python también permite el paso de argumentos por clave . En este caso, el valor se indica precedido
del nombre del argumento y un signo igual (=). Como en el caso de los argumentos inicializados, la
guía de estilo recomienda no poner espacios alrededor del signo igual.
Pueden asignarse por clave todos los argumentos de la función, pero sólo después del último argumento
asignado posicionalmente:

>>> def funcion(uno, dos, tres=3):


... print(uno, dos, tres)
...
>>> funcion('uno', tres='tres', dos='dos')
uno dos tres
>>> funcion('uno', tres='tres', 'dos')
File "<ipython-input-168-11fc1aa54ec6>", line 1
funcion('uno', tres='tres', 'dos')
^
SyntaxError: positional argument follows keyword argument

[Link]. Argumentos forzosamente posicionales o por clave.

En Python puede forzarse a que una parte de los argumentos de una función deban ser invocados
posicionalmente o por clave. Esto se consigue insertando en la definición de la función alguno de los
parámetros especiales asterisco (*) o barra inclinada (/).
Para conseguir que un grupo de argumentos deba invocarse por clave, la definición de la función debe
incluir un asterisco entre los argumentos normales y los que sólo se pueden asignar por clave, que
deberán aparecer después de los primeros.

def función(posición_o_clave, *, sólo_clave):

Un ejemplo de uso de argumentos sólo por clave podría ser la función print()7 . Esta función escribe en
pantalla todos los argumentos que se le pasan, separando los argumentos con un espacio y finalizando
la escritura con un retorno de carro:

>>> print(1, 2, 3)
1 2 3

7
Aunque realmente print() no usa este mecanismo porque usa argumentos de longitud variable que hace innecesario
forzar el uso de argumentos por clave.

73
CAPÍTULO 5. PROGRAMACIÓN PROCEDIMENTAL Y DEFINICIÓN DE FUNCIONES.

Podemos alterar el comportamiento de print() indicando el separador a usar, en vez del espacio en
blanco, usando el argumento sólo por clave sep=' '. Igualmente, podemos indicar la cadena con la que
queremos finalizar la escritura usando el argumento end='\n ':

>>> print(1, 2, 3, sep='\t|\t', end='\t...\n')


1 | 2 | 3 ...

Fijémenos en que, dado que print() escribe en pantalla todos sus argumentos posicionales, la única
manera de indicar el valor de sep y end es mediante el uso de la invocación por clave.
Análogamente, podemos exigir que un cierto número de argumentos, que forzosamente deben ser
los primeros, se indiquen exclusivamente mediante su posición. Esto lo conseguimos insertando un
argumento igual al signo de división (/)8 .

def función(sólo_posición, /, posición_o_clave):

La utilidad de los argumentos sólo posicionales es más discutible que la de los forzosamente por clave.
Por este motivo, esta característica no apareció hasta la versión 3.8 de Python, y, en buena parte, lo
hizo por simetría con éstos.
Un motivo para desear argumentos sólo posicionales es cuando el nombre del argumento no tiene
ningún significado en sí mismo (por ejemplo, min(arg1, arg2 ...) o pow(x, y)). Si se permite acceder a
estos argumentos por clave, el programador queda obligado a mantener el nombre de estos argumentos
para siempre, ya que podría haber usuarios que los utilizaran por clave. Esto impediría hacer cosas
tan útiles como reescribir las funciones con otros nombres de argumento (por ejemplo, min(primero,
siguiente ...) o pow(base, exponente)).
Todo junto, los argumentos pueden agruparse del modo siguiente:

def función(sólo_posición, /, posición_o_clave, *, sólo_clave):

5.5.3. Argumentos de longitud variable en la definición.

Un argumento precedido de asterisco (*) almacena todos los argumentos posicionales no asignados en
una tupla que se pasa en el argumento estrellado. Sólo puede haber un argumento de longitud arbitraria
o estrellado, y, aunque no hay restricciones en su colocación en la definición de la función, dado que
en invocación consumirá todos los argumentos posicionales no asignados, sólo tendrá sentido utilizar
argumentos por clave después de su utilización. Por este motivo no suele colocarse antes del último
argumento no inicializado. De hecho, lo más habitual es colocarlo en última posición de la lista de
argumentos (o penúltima, si también hay un diccionario de longitud variable).

>>> def junta(*elementos, sep='/'):


... return [Link](elementos)
...

>>> junta('uno', 'dos', 'tres')


'uno/dos/tres'

>>> junta('uno', 'dos', 'tres', sep='+')


'uno+dos+tres'

Análogamente, un diccionario de longitud variable es un argumento doblemente estrellado (es decir,


precedido de **) que recibirá todos los argumentos por clave no asignados en la invocación. Sólo puede
colocarse en último lugar en la lista de argumentos de la función:
8
Esta característica sólo está disponible a partir de la versión 3.8 de Python.

74
5.5. PASO DE ARGUMENTOS A FUNCIONES.

>>> def vendoEmbutido(saludo='Hola,', vendedor='Pep', **embutidos):


... print(saludo, 'soy', vendedor, 'y vendo embutidos.')
... print('Hoy he traído:')
... for embutido, precio in [Link]():
... print('\t', embutido, '\ta', precio, '€/kg')
...

>>> vendoEmbutido('Buenos días,', morcilla=5, secallona=12, chorizo=10)


Buenos días, soy Pep y vendo embutidos.
Hoy he traído:
morcilla a 5 €/kg
secallona a 12 €/kg
chorizo a 10 €/kg

5.5.4. Desempaquetado de argumentos en la invocación.

La contrapartida al paso de argumentos de longitud variable es el desempaquetado de argumentos . Éste


permite expandir en la invocación argumentos compuestos. Por ejemplo, si tenemos los argumentos en
una lista, al usar *lista en la invocación será equivalente a haber escrito cada uno de sus elementos
como argumento posicional de la función.
Por ejemplo, considérese las dos formas de escribir en pantalla el contenido de una lista:

>>> lista = ['uno', 'dos', 'tres',]

>>> print(lista)
['uno', 'dos', 'tres']

>>> print(*lista)
uno dos tres

En la forma doble estrellada, las entradas de un diccionario se convierten en argumentos por clave,
cuyos clave y valor coinciden con los del diccionario. Las claves del diccionario deben ser cadenas de
texto con una forma válida de identificador. Como se expanden a argumentos por clave, deben aparecer
siempre después de los posicionales:

>>> opts = {'sep': ' | ', 'end': '\t...\n'}

>>> print(*lista, **opts)


uno | dos | tres ...

5.5.5. Paralelismo entre definición e invocación.

Como se ha visto en las secciones precedentes, la definición de las funciones y su invocación comparten
estructuras comunes, con un significado en cierto modo relacionado, pero distinto:

Definición de la función Invocación de la función


Significado Posición Significado Posición
argumento Argumento no Antes de los Argumento Antes de los
inicializado. argumentos posicional. argumentos por
inicializados. clave.

75
CAPÍTULO 5. PROGRAMACIÓN PROCEDIMENTAL Y DEFINICIÓN DE FUNCIONES.

clave=valor Argumento Después de los Argumento por Después de los


inicializado con argumentos no clave. argumentos
valor por defecto. inicializados. posicionales.
*vargs Argumento de Libre, pero Desempaquetado Misma posición
longitud variable. preferentemente de tipos que los
Toma todos los después de los compuestos. argumentos
argumentos argumentos no Expande sus posicionales (antes
posicionales no inicializados. elementos como de los argumentos
asignados. argumentos por clave).
posicionales.
**dicc Diccionario de Debe ser el último Desempaquetado Misma posición
longitud variable. argumento de la del diccionario. que los
Toma todos los definición. Expande sus ítems argumentos por
argumentos por como argumentos clave (después de
clave no asignados. por clave los argumentos
(argumento=valor). posicionales).

5.6. Espacios de nombres y visibilidad.


Ya se ha comentado repetidas veces que, en sentido estricto, en Python no existen variables. Lo que
tenemos son objetos, en general inmutables, a los que podemos asociar uno o más nombres. Estos
nombres se almacenan en unas estructuras denominadas namespaces (espacios de nombres, en inglés).
Un espacio de nombres es una colección de vinculaciones entre objetos e identificadores. Habitualmente,
los espacios de nombres se construyen como diccionarios. Aunque esto es un detalle de implementación
que no afecta a su funcionalidad, sí puede ser útil tenerlo presente para comprender su funcionamiento.
Cada objeto puede estar asociado a uno o más de un nombre; sin embargo, cada nombre dentro de un
espacio de nombres debe ser único y sólo puede estar asociado a un objeto.
Cada objeto en Python tiene asociado un espacio de nombres propio; es el llamado espacio local.
Podemos intentar ver los nombres incluidos en el espacio local de un objeto invocando el comando
dir(objeto). Por ejemplo, si miramos los nombres definidos en un entero, el resultado son más de 70
nombres (en la ejecución siguiente sólo se muestran los primeros y los últimos):

>>> dir(-1)
Out[19]:
['__abs__',
'__add__',
'__and__',

...

'imag',
'numerator',
'real',
'to_bytes']

Los distintos elementos definidos en un objeto pueden ser accedidos indicando el nombre del objeto y
del elemento unidos mediante un punto. Por ejemplo, el elemento __abs__ del espacio local del objeto
-1 más arriba es una función9 :
9
Acceder al espacio de nombres de objetos como los números enteros o reales tiene sus triquiñuelas. Por ejemplo, en
este caso, debemos encerrar el número entre paréntesis

76
5.6. ESPACIOS DE NOMBRES Y VISIBILIDAD.

>>> (-1).__abs__
<method-wrapper '__abs__' of int object at 0x955e00>

La expresión method-wrapper indica que __abs__ es un método de la clase int, a la cual pertenece el
objeto -1. Como se verá en el capítulo siguiente, un método es una función asociada a un objeto o a
una clase. Podemos ejecutar la función, que en este caso ya tiene un argumento, el propio objeto, y el
resultado no es sorprendente:

>>> (-1).__abs__()
1

Así pues, todo objeto dispone de su propio espacio de nombres local... pero, en Python, todo es un
objeto de alguna clase. Incluso las propias clases son objetos. Por tanto, los espacios de nombres están
por todas partes, y son realmente una gran idea; de ahí el último principio del Zen de Python :

Namespaces are one honking great idea – let’s do more of those!

Los espacios de nombres son dinámicos. Esto quiere decir que se pueden crear vinculaciones nuevas
entre identificadores y objetos mediante asignación10 . También se crean espacios de nombres cuando
creamos objetos de la clase function usando la cláusula def, cuando creamos clases nuevas con class
o cuando importamos un módulo con import.
Como todo en Python es un objeto, los propios espacios de nombres también lo son, y un espacio
puede incluir otros. Se establece, por tanto, una jerarquía en los espacios de nombres. Desde un objeto
podemos acceder a los nombres definidos en su espacio de nombres local, pero también a los nombres
definidos en el espacio de nombres que lo incluye, y así sucesivamente.

5.6.1. ¿Paso de argumentos por valor, por referencia...? Vinculación de nombres


en la función.

En general, en los lenguajes de programación el paso de los argumentos a las funciones puede realizarse
de dos modos: por valor o por referencia. En el caso del paso de argumentos por valor, el valor de los
argumentos se copia de algún modo en el espacio de variables de la función. Cualquier modificación
del valor del argumento en el interior de la función sólo afecta a ésta; en la función invocadora, la
variable conserva su valor original. Es el caso, por ejemplo, de las funciones de C.
En algunos lenguajes que sólo permiten el paso de argumentos por valor, existen mecanismos para
permitir variar el valor de la variable en la función invocadora. Por ejemplo, en C podemos pasar la di-
rección de memoria de la variable; modificando el contenido apuntado por esta dirección conseguiremos
modificar su valor en la función superior. Es el mecanismo de punteros.
En otros casos, se permite el paso por referencia. En estos casos, si la función modifica internamente
el valor de un argumento, el valor de la variable en la función invocadora también cambia. Lenguajes
como el Pascal o el C++ permiten este tipo de argumento, junto con el visto antes de paso por valor.
En Python no se puede hablar ni de un mecanismo ni del otro. Lo que se hace es incorporar al espacio
local de la función nombres vinculados a los objetos originales. Estos objetos, por tanto, pasan a estar
referenciados en las dos funciones, pero son el mismo en ambas:

10
Aunque los objetos definidos por el propio lenguaje, como la clase int, float y demás, están protegidos y no
podemos añadir nombres o modificarlos de ninguna manera.

77
CAPÍTULO 5. PROGRAMACIÓN PROCEDIMENTAL Y DEFINICIÓN DE FUNCIONES.

>>> def f(x):


... print(id(x))
...
>>> a = 349
>>> print(id(a))
140233756648048
>>> f(a)
140233756648048

Ahora bien, si modificamos, por asignación o de otro modo, el valor del argumento en el interior de la
función, la vinculación original se destruye y se crea una nueva con el objeto asignado localmente:

>>> def f(x):


... x = 349
... print(id(x))
...
>>> f(a)
140233756647760

Por tanto, podemos concluir que el paso de argumentos en Python es semejante al paso de argumentos
por valor: desde el interior de la función no se puede modificar el valor del argumento pasado:

>>> def f(x):


... x += 1
... print(x)
...
>>> a = 999
>>> f(a)
1000
>>> print(a)
999

Esto es cierto tanto para argumentos inmutables como mutables. Así, si el argumento es mutable, y
reasignamos su valor en la función, el cambio no afecta a la función superior:

>>> def f(x):


... x = [4, 5, 6]
... print(x)
...
>>> l = [1, 2, 3]
>>> f(l)
[4, 5, 6]
>>> print(l)
[1, 2, 3]

Ahora bien, en el caso de argumentos mutables, la vinculación que se crea en la función apunta al
contenedor, no a lo que éste pueda contener. Si modificamos el contenido del argumento, ese cambio
sí se refleja también en el objeto de la función superior:

>>> def f(x):


... x[1] = 'otra cosa'
... print(x)
...
>>> l = [1, 2, 3]
>>> f(l)

78
5.6. ESPACIOS DE NOMBRES Y VISIBILIDAD.

[1, 'otra cosa', 3]


>>> print(l)
[1, 'otra cosa', 3]

En cierto modo, aunque implementado de un modo muy distinto, el paso de argumentos en Python se
comporta de un modo semejante al de C, suponiendo que hacemos el paso de los objetos inmutables
por valor, y que, en el caso de los mutables pasamos el puntero o dirección del objeto.

5.6.2. Ámbitos y visibilidad.

Un objeto, por ejemplo, una sesión interactiva de Python, accede a los nombres definidos en su espacio
local de nombres directamente; es decir, sin necesidad de indicar el espacio de nombres y el identificador
separados por punto, sino simplemente usando el identificador. Este es el modo normal de acceder a
los objetos:

>>> i = 17
>>> i
17

El conjunto de identificadores accesibles directamente desde un objeto constituye su ámbito (scope,


en inglés). Los ámbitos son jerárquicos: un objeto puede acceder a los identificadores definidos en
su propio ámbito, pero también a los definidos en el ámbito en el que se creó; y a los definidos en
el que creó al que lo creó... y así sucesivamente hasta alcanzar el ámbito más externo, que es el del
propio intérprete Python. Este ámbito más externo se denomina __builtins__, y es el que contiene los
identificadores reconocidos por el propio lenguaje, como las funciones len() o max(), las excepciones
TypeError o StopIteration, los tipos list o complex, etc.
Puede ver los objetos vinculados en __builtins__ usando el comando dir(). El resultado son más de
150 nombres, algunos de los cuales se muestran a continuación:

>>> dir(__builtins__)
['ArithmeticError',
'AssertionError',

...

'__debug__',
'__doc__',

...

'abs',
'all',

...

'type',
'vars',
'zip']

El ámbito inmediatamente interno al __builtins__ es el global, y se corresponde con el ámbito del


programa principal. Cuando ejecutamos un script de Python, el programa principal es el propio script;
en una sesión interactiva, el programa principal es la sesión. Un módulo importado también tiene su
propio ámbito global; pero, como este ámbito global es diferente del ámbito global de nuestra sesión,
sólo podremos acceder a sus nombres indicando el módulo seguido de punto y del nombre.

79
CAPÍTULO 5. PROGRAMACIÓN PROCEDIMENTAL Y DEFINICIÓN DE FUNCIONES.

Internamente a los ámbitos __builtins__ y global, cada vez que creamos una función, una clase o un
objeto de una clase se crea su propio ámbito local. Como Python permite definir funciones, clases y
objetos dentro de la definición de otras funciones o clases, los ámbitos pueden anidarse indefinidamente.
Siempre existe un único ámbito local, pero pueden existir distintos ámbitos externos, denominados
enclosing en la jerga de Python.
Cuando se hace referencia a una variable, Python busca la ubicación del identificador siguiendo siempre
el orden LEGB (Local→Enclosing→Global→Builtins):

Si se hace referencia a una variable dentro de un cierto ámbito, Python primero comprueba si
su identificador aparece en su espacio local. Si aparece en él, el objeto es accesible.
Si no aparece en el espacio de nombres del ámbito local, se busca en el espacio de nombres de la
función, clase o módulo en la que se definió el objeto. Si tampoco se encuentra ahí, se prosigue
la búsqueda en la función, clase o módulo que creo la función, clase o módulo en la que se creó
el objeto.
Este proceso se itera hasta encontrar el identificador o alcanzar el ámbito más externo. Si se
encuentra en alguno de estos ámbitos, el objeto es externo (nonlocal, en la jerga de Python).
Si el identificador no aparece en el ámbito local ni en ninguno de los externos, se busca en el
global. Si aparece en él, es un objeto global.
Si tampoco aparece en el ámbito global, se busca en el __builtins__.
Finalmente, si tampoco se encuentra en el __builtins__, se eleva la excepción NameError, indi-
cando que el nombre no está definido en el ámbito del objeto.

Se define como visibilidad de un nombre a los diferentes ámbitos desde los que es accesible. En general,
éstos serán todos los ámbitos internos al ámbito en que se vinculó el nombre.
Aunque es posible modificar este comportamiento con las cláusula global y nonlocal que se verán más
adelante, en principio, cada vez que realizamos la asignación de un nombre a un objeto, se crea una
nueva vinculación para ese nombre en el ámbito local. Si en ese ámbito ya existía una vinculación
previa, se destruye antes de crear la nueva. Si el mismo nombre ya estaba vinculado en un ámbito
externo, esta vinculación externa no se ve afectada, pero no será accesible desde el ámbito local, porque
queda tapada por la variable local. En cuanto salgamos del ámbito local, la variable externa volverá a
ser visible.
Por ejemplo, el siguiente anidamiento de funciones y clases usa el mismo identificador, nombre, en
distintos ámbitos anidados. En cada uno de ellos, el identificador hace referencia a un objeto distinto:

>>> nombre = 'Identificador "nombre" en el ámbito global'


>>> def f1():
... nombre = 'Identificador "nombre" en f1()'
... def f2():
... nombre = 'Identificador "nombre" en f2()'
... def f3():
... print('En f3(), nombre se refiere a', nombre)
... f3()
... print('En f2(), nombre se refiere a', nombre)
... f2()
... print('En f1(), nombre se refiere a', nombre)
...
>>> f1()
En f3(), nombre se refiere a Identificador "nombre" en f2()
En f2(), nombre se refiere a Identificador "nombre" en f2()
En f1(), nombre se refiere a Identificador "nombre" en f1()
>>> print('En el ámbito global, nombre sigue refiriéndose a', nombre)
En el ámbito global, nombre sigue refiriéndose a Identificador "nombre" en el ámbito global

80
5.6. ESPACIOS DE NOMBRES Y VISIBILIDAD.

Gráficamente, el identificador nombre se usa para designar objetos distintos en cada ámbito:

Fijémonos que, dentro de un ámbito concreto, el significado original del identificador se pierde al crear
un objeto nuevo con el mismo nombre; pero, al salir del ámbito, el significado original se recupera. De
hecho, nunca dejó de estar ahí.
La resolución de nombres se realiza de manera dinámica: el identificador no tiene porqué estar vincu-
lado en el momento de definir una función o clase; basta con que lo esté en el momento de acceder a
él. Por ejemplo, la función siguiente hace referencia a la variable nombre sin definirla localmente:

>>> def funcion():


... print(nombre)
...

Si la ejecutamos, tenemos error:

>>> funcion()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 2, in funcion
NameError: name 'nombre' is not defined

Pero, si definimos la variable nombre antes de ejecutar la función, el resultado es correcto:

>>> nombre = 'Este es un "nombre" global'


>>> funcion()
Este es un "nombre" global

5.6.3. Acceso a objetos de ámbitos superiores.

En principio, cuando se realiza una vinculación de nombres en un cierto ámbito, esta vinculación es
accesible desde cualquier otro ámbito interno. Ahora bien, una nueva vinculación al mismo nombre
en un ámbito interno no modifica la vinculación original; simplemente crea una nueva que la tapa

81
CAPÍTULO 5. PROGRAMACIÓN PROCEDIMENTAL Y DEFINICIÓN DE FUNCIONES.

en ese ámbito y los que cuelguen de él. En otras palabras, en principio no es posible modificar las
vinculaciones realizadas en ámbitos más externos al actual.
Existen, no obstante, dos cláusulas que permiten acceder a nombres definidos en ámbitos superiores:
la cláusula global, para acceder a los nombres del ámbito global; y la cláusula nonlocal, que permite
acceder a nombres de ámbitos intermedios.

[Link]. Acceso a objetos del ámbito global.

En Python es posible acceder a los objetos del espacio de nombres global declarando un identificador
como global. La sintaxis es sencilla: antes de cualquier uso del identificador o identificadores a usar
como globales hemos de incluir la orden:

global identificador [,...]

Pueden declararse como globales varios identificadores al mismo tiempo separándolos con comas.
Cuando se hace una asignación a un identificador declarado como global, ésta se realiza en el espacio de
nombres global. No es necesario que el identificador estuviera vinculado previamente; si no lo estaba,
podemos hacerlo desde dentro de la propia función.

>>> def f():


... global nombre
... nombre = 41
...
>>> nombre
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
NameError: name 'nombre' is not defined

>>> f()
>>> nombre
41

Aunque para poder acceder al objeto vinculado al identificador, éste sí debe estar definido en el ámbito
global previamente.

>>> def f():


... global nombre
... print(nombre)
...
>>> f()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 3, in f
NameError: name 'nombre' is not defined

>>> nombre = 'Este es un nombre "global"'


>>> f()
Este es un nombre "global"

Intentar declarar como global un identificador ya usado previamente en el ámbito local o en uno
externo intermedio provoca error de sintaxis:

>>> def f():


... nombre = 'Esto es un nombre "local"'
... global nombre

82
5.6. ESPACIOS DE NOMBRES Y VISIBILIDAD.

...
File "<stdin>", line 3
SyntaxError: name 'nombre' is assigned to before global declaration

[Link]. Acceso a objetos de un ámbito externo.

En Python también es posible acceder a los objetos de los espacios de nombres intermedios entre el
local y el global. La sintaxis es semejante al de la definición de identificadores globales, pero usando
la declaración nolocal:

nonlocal identificador [,...]

Pueden declararse como no locales varios identificadores al mismo tiempo separándolos con comas.
Contrariamente al caso de los identificadores globales, los declarados como no locales deben existir
con anterioridad en un espacio de nombres que no puede ser ni el local ni el global. Si existe en más
de uno, se usará el más interno posible:

>>> def f1():


... nombre = 'Identificador "nombre" en la función "f1"'
... def f2():
... def f3():
... nonlocal nombre
... print(nombre)
... nombre = 'Cambiamos el identificador "nombre" dentro de "f3"'
... f3()
... f2()
... print(nombre)
...
>>> f1()
Identificador "nombre" en la función "f1"
Cambiamos el identificador "nombre" dentro de "f3"

[Link]. Unicidad de las vinculaciones en cada ámbito.

Aunque el uso de las declaraciones global y nonlocal permite acceder a vinculaciones realizadas en
ámbitos externos al actual, en cada ámbito sólo se puede tener una vinculación para cada nombre. Es
decir, si usamos un nombre en un ámbito, ya no podremos usar ese mismo nombre en ninguno más.
Por ejemplo, las dos situaciones siguientes dan lugar a error (aunque, sobre todo la primera, pueda
costar ver porqué en primera instancia):

Intentar crear una variable local usando el valor de una variable externa del mismo nombre:

>>> a = 1
>>> def f():
... a = a + 1
...
>>> f()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 2, in f
UnboundLocalError: local variable 'a' referenced before assignment

83
CAPÍTULO 5. PROGRAMACIÓN PROCEDIMENTAL Y DEFINICIÓN DE FUNCIONES.

Lo que ha ocurrido es que en la orden a = a + 1 el valor original de a es el que tiene en el ámbito


global, pero, a continuación, intentamos crear una variable del mismo nombre en el ámbito local
al realizar la asignación.

• Este problema se puede resolver declarando a como global en la función f():

>>> a = 1
>>> def f():
... global a
... a = a + 1
...
>>> f()

• La variable a que hemos modificado es la del ámbito global, así que, si miramos su valor
fuera de la función, éste reflejará los cambios sufridos en el interior de la misma:

>>> a
2

También provoca error crear una vinculación local e intentar declarar el mismo nombre como
externo más tarde:

>>> def f():


... a = 1
... global a
...
File "<stdin>", line 3
SyntaxError: name 'a' is assigned to before global declaration

84
Capítulo 6

Programación orientada a objeto.

La programación orientada a objeto es un paradigma de programación en la que los datos y las funciones
que los manejan son encapsulados en estructuras denominadas clases. Estas clases permiten definir
objetos miembros de las mismas, de manera análoga a como se crean objetos de los distintos tipos
estándar del lenguaje, pero cuyo comportamiento está gobernado por la definición de la clase.
Los conceptos básicos de la programación orientada a objeto aparecen en los años 50 del siglo XX, pero
no es hasta los años 80 que cobran popularidad, con C++, convirtiéndose en el paradigma dominante
en los años 90, con la eclosión del propio C++ y de Java. Hoy en día, aunque su popularidad ha
disminuido, continúa siendo el paradigma de referencia.
Sin embargo, la programación orientada a objeto también ha sido sometida a críticas demoledoras.
Por ejemplo, es conocida la aversión al C++ y Java de Linus Torvalds , creador del núcleo de GNU/-
Linux y del sistema de gestión de versiones Git1 . El principal motivo de estas críticas es el hecho
de que, contrariamente a los motivos que ayudaron a popularizar el paradigma, los programas que
lo usan acaban a menudo siendo galimatías indescifrables en los que su mantenimiento o, incluso, su
comprensión, resulta prácticamente imposible. ”Se deseaba lograr un mayor grado de abstracción, pero
se alcanzó la abstracción más absoluta”.

6.1. Clases y objetos en Python.

Python es un lenguaje orientado a objetos puro; es decir, en él todo es tratado consistentemente como
un objeto. Por ejemplo, en Python nunca ha habido una distinción acusada entre tipos y clases, pero,
a partir de Python 3, un tipo es simplemente una clase más. De hecho, todo en Python 3, incluidas
las funciones, son clases:

>>> def funcion(): pass

>>> type(funcion)
<class 'function'>

En su forma más simple, una clase se define con la cláusula class , seguida del nombre de la clase y el
bloque que la define, con las dos formas habituales:

class Clase: bloque

class Clase:
bloque

1
Aunque hay que señalar que Torvalds sí ha incluido código desarrollado en Python en Git, e incluso ha afirmado
que escogió este lenguaje para enseñar programación a sus hijas.

85
CAPÍTULO 6. PROGRAMACIÓN ORIENTADA A OBJETO.

Donde bloque no puede estar vacío. Así, la clase más sencilla que podemos crear tiene un bloque que
no hace nada (podemos usar el comando pass o una Ellipsis (...) para ello):

>>> class Clase: ...

Una cadena de texto no vinculada a ningún nombre como primera sentencia del bloque de definición
de la clase se convierte en su documentación, y aparecerá reflejada al ejecutar help(Clase). Como esta
cadena de texto ya es una sentencia en sí misma, el bloque de definición puede estar formado por ella
nada más:

>>> class Clase:


... """
... Esta clase sólo tiene documentación.
... """
...

>>> help(Clase)
Help on class Clase in module __main__:

class Clase([Link])
| Esta clase sólo tiene documentación
|
| Data descriptors defined here:
|
| __dict__
| dictionary for instance variables (if defined)
|
| __weakref__
| list of weak references to the object (if defined)

Las clases permiten encapsular objetos en su definición. Todo objeto, del tipo que sea, creado en el
bloque de código de la definición de la clase es un atributo de la misma. Suele hacerse la distinción entre
los atributos que son funciones y el resto. A los primeros se les denomina métodos; siendo habitual
reservar el nombre de atributo para los segundos.

>>> class Clase:


...: atributo = 12
...: def metodo(self):
...: pass

Accedemos a los atributos y métodos de una clase, o de los objetos pertenecientes a la misma, indicando
el nombre de la clase u objeto seguido de un punto y del nombre del atributo o método. El punto
puede estar rodeado por un número arbitrario de espacios:

>>> [Link]
12

>>> Clase . metodo


<function __main__.[Link]()>

6.1.1. Clases y espacios de nombres.

En esencia, una clase es un espacio de nombres en el que alguno de sus elementos tiene un compor-
tamiento peculiar. Este espacio de nombres se almacena en el atributo __dicc__, que se crea automá-

86
6.1. CLASES Y OBJETOS EN PYTHON.

ticamente al crear la clase y, como su nombre indica, es un objeto con un significado especial para el
intérprete:

>>> Clase.__dict__
mappingproxy({'__module__': '__main__',
'atributo': 12,
'metodo': <function __main__.[Link]()>,
'__dict__': <attribute '__dict__' of 'Clase' objects>,
'__weakref__': <attribute '__weakref__' of 'Clase' objects>,
'__doc__': None})

Vemos que el espacio de nombres de la clase incorpora los dos atributos creados en su definición,
además de otros cuatro que se crean automáticamente y tienen un significado especial:

__module__
Es el módulo en el que se ha creado la clase. En el caso de ser una sesión interactiva, su
nombre es '__main__'. En el caso de ser un módulo importado, es el nombre del módulo.

__dict__
El el diccionario en el que se almacena el espacio de nombres de la clase.

__weakref__
Las weakrefs de un objeto son referencias al mismo que el recolector de basura no tiene
en cuenta a la hora de decidir si libera la memoria asociada o no. Se usa para mejorar la
eficiencia de los programas; por ejemplo, en el acceso a colecciones grandes de objetos. Por
ahora, no es necesario conocer mucho más acerca de su utilidad y/o funcionamiento.

__doc__ Cadena de documentación de la clase.

Esto nos proporciona un mecanismo alternativo al acceso a los atributos de la clase: además del uso del
punto como separador de los nombres de la clase y el atributo, también podemos hacerlo accediendo
al diccionario de la clase usando como clave el nombre del atributo en forma de cadena de texto2 :

>>> Clase.__dict__['atributo']
12

El espacio de nombres de una clase es análogo al espacio de nombres local de una función: es en él en
el que se crean y acceden los atributos definidos por la propia clase. Sin embargo, y como ocurre en
el caso de las funciones, también se puede acceder a otros nombres definidos en ámbitos externos a la
propia clase. En concreto, las clases creadas por el usuario heredan toda una serie de atributos de la
clase object, definida en el espacio de nombres __builtins__.
Podemos ver los objetos accesibles por la clase usando el comando dir(). Incluso una clase tan sencilla,
como la creada más arriba, viene de serie con acceso a unos veinticinco objetos:

>>> dir(Clase)
['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__',
'__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__',
'__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__',
'__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__',
'__str__', '__subclasshook__', '__weakref__', atributo, metodo]

2
De hecho, aún habría, por lo menos, otro modo más para acceder a los atributos de una clase: los métodos mágicos
__getattribute__() y __setattr__...()

87
CAPÍTULO 6. PROGRAMACIÓN ORIENTADA A OBJETO.

Además de en el momento de definición de la clase, Python también permite crear o modificar atributos
en cualquier momento mediante asignación:

>>> [Link] = 33
>>> Clase.__dict__
mappingproxy({'__module__': '__main__',
'atributo': 12,
'metodo': <function __main__.[Link]()>,
'__dict__': <attribute '__dict__' of 'Clase' objects>,
'__weakref__': <attribute '__weakref__' of 'Clase' objects>,
'__doc__': None,
'valor': 33})

Evidentemente, si modificamos alguno de los que tienen un significado especial, el comportamiento de


la clase variará en consonancia:

>>> Clase.__doc__ = 'Nueva cadena de documentación'


>>> help(Clase)
Help on class Clase in module __main__:

class Clase([Link])
| Nueva cadena de documentación
|
| Data descriptors defined here:
|
| __dict__
| dictionary for instance variables (if defined)
|
| __weakref__
| list of weak references to the object (if defined)
|
| ----------------------------------------------------------------------
| Data and other attributes defined here:
|
| valor = 12

6.1.2. Creación de objetos (y espacios de nombres).

Como se indicó al principio del capítulo, en Python una clase es básicamente un espacio de nombres
con comportamientos raritos. El principal de estos comportamientos es la creación de objetos. Esto se
hace invocando el nombre de la clase como si fuera una función que nos devuelve el objeto:

>>> objeto = Clase()

>>> type(objeto)
<class '__main__.Clase'>

Los objetos creados de este modo son, también, espacios de nombres con comportamientos raritos. En
principio, el espacio de nombres del objeto está vacío:

>>> objeto = Clase()

>>> objeto.__dict__
{}

88
6.1. CLASES Y OBJETOS EN PYTHON.

Ahora bien, su visibilidad incluye todos los objetos visibles desde la clase:

>>> dir(objeto) == dir(Clase)


True

Lo que da sentido al mecanismo de clases y objetos en Python es que, si modificamos un atributo del
objeto, éste pasa a ser propiedad exclusiva del mismo. Es decir, si alteramos el valor de un atributo,
el efecto es crear una nueva vinculación en el espacio de nombres del objeto, sin afectar al espacio de
nombres original de la clase:

>>> [Link] = 'Ahora tenemos un atributo distinto para clase y objeto'

>>> objeto.__dict__
{'atributo': 'Ahora tenemos un atributo distinto para clase y objeto'}

>>> Clase.__dict__
mappingproxy({'__module__': '__main__',
'atributo': 12,
'metodo': <function __main__.[Link]()>,
'__dict__': <attribute '__dict__' of 'Clase' objects>,
'__weakref__': <attribute '__weakref__' of 'Clase' objects>,
'__doc__': None,
'valor': 33})

Aunque cambiemos el valor del atributo en la clase original, el valor en el objeto no varía, porque son
objetos con el mismo nombre, pero en espacios distintos:

>>> [Link] = 'Ahora cambia el atributo en la clase'

>>> objeto.__dict__
{'atributo': 'Ahora tenemos un atributo distinto para clase y objeto'}

>>> Clase.__dict__['atributo']
'Ahora cambia el atributo en la clase'

De algún modo, la vinculación del nombre atributo en el espacio de nombres del objeto tapa, pero no
modifica, la vinculación de ese mismo nombre en el espacio de nombres de la clase (y de los nuevos
objetos que creemos de ella).
Es más, y esto es algo que puede resultar muy sorprendente para programadores en otros lenguajes
orientados a objeto como C++ o Java, podemos añadir atributos a una clase en cualquier momento,
y estos nuevos atributos serán visibles por todos los objetos de la clase; incluso por los que hubieran
sido creados con anterioridad:

>>> [Link]
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: 'Clase' object has no attribute 'otro'

>>> [Link] = 'otro'

>>> [Link]
'otro'

En verdad, no es que todos los objetos de la clase adquieran un nuevo atributo, sino que todos ellos
pueden acceder a los atributos definidos en el espacio de nombres de la clase.

89
CAPÍTULO 6. PROGRAMACIÓN ORIENTADA A OBJETO.

Muchos de estos detalles pueden pasar desapercibidos cuando se programa en Python ciñéndonos
a conceptos de otros lenguajes como C++. En C++ no se pueden añadir atributos a una clase
después de definida, y nadie nos obliga a hacerlo en Python. De hecho, no será habitual hacerlo. Sin
embargo, comprender cómo funciona internamente Python no sólo permite comprender la profunda
diferencia entre él y otros lenguajes de programación, sino que también pone alcance del programador
herramientas muy potentes que no están disponibles en ellos.

6.1.3. Métodos en clases y objetos.

Se denomina método a una función que es un atributo de una clase o de un objeto. Podemos crear
métodos de una clase por cualquiera de los mecanismos posibles; por ejemplo, mediante asignación.
Sin embargo, el modo más habitual es hacerlo definiendo la función en el bloque de código de definición
de la clase (de manera análoga a cómo lo haríamos en C++ o Java).
Desde el punto de vista de la clase, un método es una función como otra cualquiera. Por ejemplo,
podemos definir un método que devuelva la suma de dos valores:

>>> class Clase():


... def suma(a, b):
... return a + b
...
>>> [Link](12, 54)
66

Ahora bien, desde el punto de vista de los objetos de la clase, la cosa cambia radicalmente: al invocar
un método de un objeto, el propio objeto se convierte en su primer argumento:

[Link](args...) ≡ [Link](objeto, args...)

Nótese que el número de argumentos que debemos explicitar al invocar el método es distinto en un
caso y otro, ya que, cuando la función se invoca como método del objeto, éste es un primer argumento
implícito. Por este motivo, es habitual, y muy recomendable, denominar a este primer argumento con
el nombre self, cuyo uso es semejante al de this en otros lenguajes como C++.
Ampliamos el ejemplo anterior para poder hacer la suma de objetos de la clase. Para ello debemos
definir un atributo que permita ser sumado:

>>> class Clase():


... valor = 4
... def suma(self, otro):
... return [Link] + otro
...
>>> objeto = Clase()
>>> [Link](objeto, 5)
9
>>> [Link](5)
9

De hecho, este es el mecanismo usado en los operadores del lenguaje Python: cada operador es un
método de la clase correspondiente. En concreto, un método mágico, porque su interpretación como
operador corre de la cuenta del propio lenguaje. Por ese motivo, su nombre empieza y acaba siempre
por dos subrayados. A la implementación de un operador como una función se le denomina sobrecarga
del operador.
Por ejemplo, el método __add__() es la sobrecarga del operador suma (+). Como cualquier otro método,
podemos invocarlo como método de la clase o de uno de sus objetos:

90
6.2. EJEMPLO DE APLICACIÓN: LA CLASE VECTOR.

>>> int.__add__(2, 3)
5
>>> 2 . __add__(3)
5

En este caso, el espacio entre el 2 y el punto es necesario para evitar que el intérprete se arme la picha
un lío...

6.2. Ejemplo de aplicación: la clase Vector.

Para ilustrar los mecanismos de la programación orientada a objeto se va a utilizar el diseño e imple-
mentación de la clase Vector, que, como su nombre indica, permitirá construir objetos semejantes a
los vectores usados en álgebra.
En Python ya existen estructuras semejantes a los vectores: la lista, como objeto mutable, y la tupla,
como inmutable. Pero su funcionalidad dista mucho de la de los vectores algebraicos. Por ejemplo,
la suma de dos listas de números no es la lista con las sumas de los elementos de cada lista, sino la
concatenación de las dos:

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

>>> vector + [4, 5, 6]


[1, 2, 3, 4, 5, 6]

En este ejemplo sólo escribiremos métodos mágicos . Estos métodos, que se caracterizan porque su
nombre viene precedido y seguido por dos subrayados (__metodo_mágico__()), permiten tareas como la
sobrecarga de operadores aritméticos o lógicos, como la suma (con el método __add__()); la implemen-
tación de comandos como len() (con __len__()); o cosas tan sofisticadas como el acceso a elementos
de un objeto compuesto sobrecargando los corchetes, [clave], (con __getitem__() y __setitem__()).

Fichero algebra/[Link]

Crearemos la clase vector en el fichero algebra/[Link]. Inicialmente lo ejecutaremos como un


script, con la orden exec(open('algebra/[Link]').read()), o, si se ejecuta desde ipython, con
el comando run algebra/[Link]. Más adelante usaremos el directorio algebra para formar una
biblioteca algebraica, que podría contener, además, los ficheros [Link] o [Link].
Inicializamos la clase con su cadena de documentación:
algebra/[Link]
1 class Vector:
2 """
3 Clase usada para trabajar con vectores sencillos
4 """

6.2.1. Constructor de la clase.

El constructor de una clase es el método que devuelve un objeto de la misma al invocarla como si
fuera una función, y recibe por nombre __init__()3 . El constructor sólo puede devolver None, ya que
3
En verdad, y a partir de la versión Python 3, __init__() no es el constructor sino el inicializador del objeto.
El auténtico constructor se llama __new__(). Cuando creamos un objeto, Python invoca __new__() para crearlo y, a
continuación, __init__() para inicializarlo. No obstante, y salvo en situaciones muy especiales, el constructor adecuado
es el proporcionado por Python, y el programador sólo debe preocuparse de __init__().

91
CAPÍTULO 6. PROGRAMACIÓN ORIENTADA A OBJETO.

de la creación del objeto se encarga Python. Devolver cualquier cosa distinta de None provoca un error
de ejecución.
En el caso de la clase vector, vamos a definir un constructor que tome por argumento un iterable y
almacene sus valores en una lista:
algebra/[Link]
5 def __init__(self, iterable=None):
6 [Link] = [valor for valor in iterable]
7
8 return None # Orden superflua

6.2.2. Representación textual: métodos __repr__() y __str__().

Suele ser útil configurar la representación textual de los objetos de una clase, ya que la representación
por defecto no suele ser de mucha utilidad:

>>> vector = Vector([0, 1, 2, 3, 4, 5])

>>> vector
<__main__.Vector object at 0x7fd70a7ed128>

Para obtener una representación más adecuada de los objetos, Python proporciona dos métodos dis-
tintos, __repr__() y __str__() :

__repr__(self):
Es la representación oficial del objeto. Lo ideal es que sea una representación inequívoca
del mismo, de manera que podamos reconstruirlo a partir de la misma (por ejemplo, con
cut-and-paste). Es accesible mediante el comando repr(), el cual usamos para representar la
lista en sí de los elementos del vector:
algebra/[Link]
10 def __repr__(self):
11 return 'Vector(' + repr([Link]) + ')'

Podemos visualizar la representación del vector usando el comando repr() o, simplemente,


escribiendo su nombre:

>>> vector
Vector([0, 1, 2, 3, 4, 5])

Fijémonos en que, si copiamos el resultado y lo ejecutamos en Python, obtendremos un objeto


idéntico al representado.
__str__(self):
Es la representación bonita del objeto. En caso de no definirse, se usa __repr__() en su lugar.
Es accesible mediante el comando str(), que utilizamos para visualizar los elementos de la
lista:
algebra/[Link]
13 def __str__(self):
14 return str([Link])

Ésta es la representación que se usa por defecto al ejecutar el comando print():

92
6.2. EJEMPLO DE APLICACIÓN: LA CLASE VECTOR.

>>> print(vector)
[0, 1, 2, 3, 4, 5]

6.2.3. Acceso a los elementos: métodos __getitem__(), __setitem__() y __len__().

Los métodos __getitem__() y __setitem__() permiten acceder a los elementos del objeto para obtener
su valor y modificarlo, respectivamente. __len__() devuelve la longitud del vector.

__getitem__(self, key):
Devuelve el elemento key de un tipo compuesto o contenedor:
algebra/[Link]
16 def __getitem__(self, key):
17 return [Link][key]

También permite lonchas (desde el principio o desde el final):

>>> vector[1: 4]
[1, 2, 3]

>>> vector[-2: -5: -1]


[4, 3, 2]

Al definir el método __getitem__, la clase pasa a ser iterable, permitiendo su uso en bucles
for o en comprensiones:

>>> [valor ** 2 for valor in vector]


[0, 1, 4, 9, 16, 25]

También permite el uso de comandos como sum() o max(), pero no len(). Para éste último es
necesario definir el método __len__():

>>> sum(vector)
15

>>> len(vector)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
----> 1 len(vector)

TypeError: object of type 'Vector' has no len()

__setitem__(self, key, value):


Escribe el valor value en la posición key del vector.
algebra/[Link]
19 def __setitem__(self, key, value):
20 [Link][key] = value

Definiendo este método es posible asignar valores a los elementos, o lonchas, del vector:

93
CAPÍTULO 6. PROGRAMACIÓN ORIENTADA A OBJETO.

>>> vector[: : 2] = [0] * 3

>>> vector
Vector([0, 1, 0, 3, 0, 5])

__len__(self):
Devuelve el número de elementos del vector. Es necesario para poder ejecutar el comando
len().
algebra/[Link]
22 def __len__(self):
23 return len([Link])

En este caso:

>>> len(vector)
6

6.2.4. Sobrecarga de los operadores aritméticos.

Los distintos operadores aritméticos (+, -, *, /, etc.) pueden sobrecargarse usando sus respectivos
métodos mágicos para la emulación de tipos numéricos .
Los métodos asociados a operadores tienen nombres que recuerdan el cometido del operador, pero,
como son mágicos, están precedidos y seguidos por dos subrayados (__oper__()).
Al indicar una operación con dos argumentos, Python busca el método asociado al operador para el
tipo del primero de los operandos y, si existe, lo ejecuta. El primer operando se corresponde con el
primer argumento, implícito, del método, y suele ponérsele el nombre self. Para el segundo operando,
el nombre recomendado es other:

self oper other ≡ self.__oper__(other) ≡ Clase.__oper__(self, other)

Por ejemplo, el método mágico asociado a la suma es __add__(), que toma dos argumentos: self, que
es el propio vector, y other que es el elemento a sumar al vector.
En álgebra de vectores podemos sumar dos vectores o una constante al vector. Determinamos si
estamos en un tipo u otro de suma con el comando isinstance() , que tiene por argumentos un objeto
y una tupla de tipos o clases. El comando devuelve True si el objeto es miembro de alguno de los tipos
enumerados en la tupla, o False si no lo es.
algebra/[Link]
25 def __add__(self, other):
26 if isinstance(other, (int, float, complex)):
27 return Vector(uno + other for uno in self)
28 else:
29 return Vector(uno + otro for uno, otro in zip(self, other))

Ahora podemos usar el operador suma (+) como lo haríamos con cualquier tipo numérico:

>>> vector + 4
Vector([4, 5, 6, 7, 8, 9])

>>> vector + [0, 2, 0, 2, 0, 2]


Vector([0, 3, 2, 5, 4, 7])

94
6.2. EJEMPLO DE APLICACIÓN: LA CLASE VECTOR.

[Link]. Sobrecarga de operadores, reflejo e inmediatez.

Si intentamos darle la vuelta a los operandos, y sumarle un vector a un número o lista de números,
Python se quejará de que los tipos no son compatibles con esa operación. Esto es así porque hemos
definido el comportamiento de la clase Vector para que esté preparada para que se le sume números y
listas de números, pero estos tipos de datos no tienen por qué estar preparados para que se les sume
vectores:

>>> vector + 1
Vector([1, 2, 3, 4, 5, 6])

>>> 1 + vector
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
----> 1 1 + vector

TypeError: unsupported operand type(s) for +: 'int' and 'Vector'


Traceback (most recent call last)
File "<stdin>", line 1, in <module>
----> 1 len(vector)

TypeError: object of type 'Vector' has no len()

En verdad, Python no ha elevado la excepción TypeError de buen principio. Lo que ha hecho ha sido
ejecutar el método int.__add__(vector), pero éste, al no conocer el tipo de su segundo operando, ha
elevado la excepción NotImplemented. Python, entonces, intenta una estrategia alternativa: ejecutar, si
existe, el método vector.__radd__(int). Es decir, un método de la clase vector, pero que se invoca con
los argumentos intercambiados (reversed, en inglés). Como no lo ha encontrado, entonces sí ha elevado
la excepción TypeError.
Todo método mágico de emulación de tipos numéricos debe elevar la excepción NotImplemented cuando
no reconoce el tipo de su segundo argumento, de manera que exista la posibilidad de encontrar el
método reflejado del tipo del segundo argumento.
En el caso de la suma, y como ésta es conmutativa, el método __radd__() es igual al __add__(), así que
su implementación es sumamente sencilla:
algebra/[Link]
31 __radd__ = __add__

Con esta simple definición, ya somos capaces de sumarle un vector a un número o lista de números:

>>> 1 + vector
Vector([1, 2, 3, 4, 5, 6])

>>> [1, 0, 1, 0, 1, 0] + vector


Vector([1, 1, 3, 3, 5, 5])

Por otro lado, los operadores inmediatos suelen ser equivalentes a una asignación seguida de la opera-
ción aplicada sobre el propio operando:
self oper= other ≡ self = self oper other

Podría ocurrir, no obstante, que estuviéramos interesados en un comportamiento distinto. Aunque esto
es, en cierto modo, contrario a la propia naturaleza de los operadores inmediatos, Python proporciona
un mecanismo para realizarlo, los operadores inmediatos, que se corresponden con el método mágico
de mismo nombre que el operador, pero precedido de la letra i. Por ejemplo, __iadd__().

95
CAPÍTULO 6. PROGRAMACIÓN ORIENTADA A OBJETO.

La resta en la clase Vector.

Como ejemplo adicional, a continuación se muestran las funciones que permiten la resta de un vector o
una constante a un vector. Lo hacemos indirectamente, implementando primero la inversión de signo
mediante el método __neg__(), que sobrecarga el operador unario menos (-), y apoyándonos en la
definición previa de la suma:
algebra/[Link]
33 def __neg__(self):
34 return Vector([-1 * item for item in self])
35
36 def __sub__(self, other):
37 return -(-self + other)
38
39 def __rsub__(self, other): # __rsub__ ̸= __sub__ debido a no conmutatividad
40 return -self + other

Fijémonos en que no nos podamos fiar de que other soporte el operador unario de inversión de signo.
Por eso, la resta se implementa siempre cambiando el signo de self.

6.2.5. Otros métodos mágicos.

[Link]. Sobrecarga de la invocación como función: __call__().

[Link]. Acceso a atributos del objeto: __getattr__(), __setattr__() y __delattr__.

[Link]. Clases hashables e igualdad: __hash__() y __eq__().

[Link]. Cuidadito con el exceso de magia...

No es necesario definir todos los métodos mágicos; el estándar ya proporciona versiones válidas para
la mayor parte de los casos.

Sobrecargas adicionales.

El mecanismo de sobrecarga de operadores es muy goloso, pero debe manejarse con cuidado porque
puede resultar en un código difícil de leer. En los casos vistos hasta ahora, cada operador se sobrecarga
con una operación claramente relacionada con su cometido inicial. Evidentemente, no tendría sentido
sobrecargar el asterisco (*) para implementar la resta. Hay casos en los que esto no es tan evidente. En
estos casos es mejor usar un método no sobrecargado que uno que no respete, aunque sea remotamente,
el espíritu original del operador.
Por ejemplo, si queremos implementar el desplazamiento de los elementos del vector, podemos sobre-
cargar los operadores de desplazamiento de bits (<< y >>), que no deja de ser una operación semejante.
Sin embargo, no parece justificado sobrecargar estos operadores para implementar el encadenado de
vectores. En este último caso, y dado que el operador suma (+) ya tiene una aplicación más evidente,
lo mejor es usar un método no sobrecargado; por ejemplo, [Link]().
Si un operador no tiene sentido para objetos de una cierta clase, debemos dejarlo sin definir. Python,
al no encontrar el método correspondiente, elevará la excepción NotImplemented. Sin embargo, si el
problema es que nuestro método no soporta el tipo de datos del segundo operando, lo que deberá
hacer es elevar él la excepción NotImplemented. De este modo damos la oportunidad a Python de
probar a ver si el método reflejado del segundo operando sí es capaz de manejar la operación.

96
6.3. POLIMORFISMO, HERENCIA Y TIPADO PATOSO.

6.3. Polimorfismo, herencia y tipado patoso.

El polimorfismo es la propiedad por la que algo se manifiesta de formas distintas. En Python, el


polimorfismo es polimórfico (hoy estamos sembrados). Es decir, también el polimorfismo se manifiesta
de distintas formas.
Por ejemplo, tenemos funciones u operadores que manejan los datos de manera diferente en función
de su naturaleza. Así, el comando len() proporciona el número de caracteres, cuando su argumento
es una cadena de texto, o el número de elementos, cuando es una lista:

>>> len('cadena de texto')


15
>>> len(['lista', 'de', 'cadenas', 'de', 'texto'])
5

Un tipo de polimorfismo diferente es cuando clases distintas tienen métodos con el mismo nombre,
aunque hagan cosas diferentes. En ese caso, cualquier operación que requiera la existencia de un
método de ese nombre podrá realizarse sobre objetos de cualquiera de las clases:

>>> class Pato:


... def hola(self):
... print('Hola, soy un pato.')
...
>>> class Gato:
... def hola(self):
... print('Hola, soy un gato.')
...
>>> def saluda(bicho):
... [Link]()
...
>>> donald = Pato()
>>> felix = Gato()
>>>
>>> saluda(donald)
Hola, soy un pato.
>>> saluda(felix)
Hola, soy un gato.

Curiosamente, este tipo de polimorfismo es la base del denominado tipado patoso que veremos en el
apartado 6.3.2; aunque no existe nada en Python que merezca el nombre de tipado gatuno.

6.3.1. Herencia.

En el ejemplo de patos y gatos del apartado anterior se ha definido explícitamente el método hola()
para cada uno de ellos. Si, además de patos y gatos, también tuviéramos ratas en el corral, tendríamos
que definir su propio método hola() para que pudieran saludar.
Una alternativa es el uso de la herencia. Ésta consiste en la creación de una clase superior, que
llamaremos Bichos, y que sea heredada por cada una de las clases que pertenezcan a la misma. Las
clases herederas reciben inicialmente todos los atributos y métodos de la clase superior, pero también
pueden definir versiones específicas de los mismos que los sustituyan.
Para indicar que una clase hereda los atributos de otra superior, colocamos el nombre de esta entre
paréntesis en la definición:

97
CAPÍTULO 6. PROGRAMACIÓN ORIENTADA A OBJETO.

>>> class Bichos:


... def hola(self):
... print('Hola, soy un bicho.')
...
>>> class Pato(Bichos):
... def hola(self):
... print('Hola, soy un pato.')
...
>>> class Gato(Bichos):
... def hola(self):
... print('Hola, soy un gato.')
...
>>> class Rata(Bichos):
... "Para la rata no definimos el método hola()"
...
>>> donald, felix, minnie = Pato(), Gato(), Rata()
>>> saluda(donald)
Hola, soy un pato.
>>> saluda(felix)
Hola, soy un gato.
>>> saluda(minnie)
Hola, soy un bicho.

Como podemos ver, donald y felix han usado el saludo propio de su clase, mientras que minnie ha
usado el saludo genérico de los bichos.
Aunque una clase redefina un atributo de la clase superior, el atributo superior sigue siendo accesible
mediante la función super(), que devuelve la clase de la cual se hereda:

>>> class Rana(Bichos):


... def hola(self):
... super().hola()
... print("En concreto, soy una maldita rana.")
...
>>> gustavo = Rana()
>>> saluda(gustavo)
Hola, soy un bicho.
En concreto, soy una maldita rana.

Vemos que gustavo usa un saludo personalizado que, a su vez, llama al saludo genérico.
El proceso de herencia puede extenderse horizontal y verticalmente. Así, una clase puede heredar de
más de una clase (herencia múltiple horizontal) colocando las distintas clases superiores separadas por
comas. A su vez, cada clase superior puede ser heredera de otra u otras.
Por ejemplo, las ranas podrían ser herederas tanto de los bichos como de los verdes, y, su vez, los
bichos podrían ser herederos de los seres y los verdes de los colores:

>>> class Colores:


... pass
...
>>> class Verdes(Colores):
... pass
...
>>> class Seres:
... pass
...
>>> class Bichos(Seres):
... pass
...

98
6.3. POLIMORFISMO, HERENCIA Y TIPADO PATOSO.

>>> class Ranas(Bichos, Verdes):


... pass
...

Cuando queramos acceder a un atributo de objeto de la clase Ranas, primero lo buscaremos en el


espacio de nombres del propio objeto; si no se encuentra, lo buscaremos en el de la clase Ranas; si
sigue sin aparecer, lo buscaremos en la clase superior de Ranas, que es Bichos; si tampoco aparece ahí,
seguiremos buscando en Verdes y en Colores.
La búsqueda es siempre primero en vertical y luego en horizontal. Puede ser un poco follón, la verdad...
Podemos consultar el orden en que se realiza la búsqueda invocando el método mro(), que devuelve el
valor del atributo __mro__ (del inglés method resolution order) de la clase:

>>> [Link]()
[<class '__main__.Ranas'>, <class '__main__.Bichos'>, <class '__main__.Seres'>,
<class '__main__.Verdes'>, <class '__main__.Colores'>, <class 'object'>]

Todos los objetos son herederos, en última instancia, de la clase object, que está definido en el ámbito
__builtins__, es decir, el propio lenguaje.

[Link]. Todos los objetos pertenecen a alguna clase.

En Python, todo (hay que insistir aún más, todo), es un objeto que pertenece a alguna clase. Eso
incluye los tipos básicos, como int o list.
Por ejemplo, la suma de reales está implementada como el método mágico __add__() de la clase float:

>>> float.__add__
<slot wrapper '__add__' of 'float' objects>

Eso nos permite realizar la suma de reales invocando el método en lugar de su operador asociado:

>>> 4..__add__(2)
6.0

Fijémonos en que se han escrito dos puntos: el primero, para indicar que se trata de un objeto float,
el segundo, para acceder al atributo __add__().
Eso quiere decir que no podemos hacerlo directamente con enteros, porque al colocar el primer punto,
Python lo interpretará como indicador de float, y luego no sabrá qué hacer con el nombre del método.
Sí podemos hacerlo si encerramos el entero entre paréntesis:

>>> (4).__add__(2)
6

La herencia nos permite crear clases que se parezcan mucho a los tipos básicos, pero que tengan un
comportamiento rarito. Por ejemplo, podemos crear un tipo de enteros que intercambie la suma y la
resta:

99
CAPÍTULO 6. PROGRAMACIÓN ORIENTADA A OBJETO.

>>> class Raritos(int):


... def __add__(self, other): return super().__sub__(other)
... def __sub__(self, other): return super().__add__(other)
...
>>> i = Raritos(4)
>>> i # Llama al método __repr__ de 'int'
4
>>> i * 2 # Llama al método __mul__ de 'int'
8
>>> i + 1 # Llama al método __add__ de 'Raritos'
3
>>> i - 1 # Llama al método __sub__ de 'Raritos'
5

Por desgracia, Python protege los tipos básicos, con lo que no se puede redefinir de este modo la suma
y la resta de int o cualquier otro tipo del conjunto estándar. Si no fuera así, sería una manera muy
chula de gastar bromas, no siempre de buen gusto.

6.3.2. El tipado patoso.

Python soporta el llamado tipado patoso (duck typing) inspirado en un poema de principios del
siglo XX, aunque popularizado a raíz de la involucración de la CIA en el derrocamiento en 1954 del
presidente guatemalteco Juan Jacobo Árbenz Guzmán :

Si parece un pato, nada como un pato y hace cuac como un pato, entonces
probablemente sea un pato.

En programación, esto quiere decir que, habitualmente, no se hace ninguna comprobación de tipos. Es
posible que las operaciones sean compatibles con los tipos de los datos, y entonces no habrá ningún
problema. Y si no lo son, ya se encargará Python de elevar la correspondiente excepción:

>>> vector + 'cadena'


Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "/home/albino/APAV/algebra/[Link]", line 29, in __add__
return Vector(uno + otro for uno, otro in zip(self, other))
File "/home/albino/APAV/algebra/[Link]", line 6, in __init__
[Link] = [valor for valor in iterable]
File "/home/albino/APAV/algebra/[Link]", line 6, in <listcomp>
[Link] = [valor for valor in iterable]
File "/home/albino/APAV/algebra/[Link]", line 29, in <genexpr>
return Vector(uno + otro for uno, otro in zip(self, other))
TypeError: unsupported operand type(s) for +: 'int' and 'str'

Ésta es la estrategia pythónica por excelencia (además, requiere poco trabajo por parte del programa-
dor, porque le libera de tener que comprobar la validez de los tipos), y tiene ventajas increíbles.
Por ejemplo, imaginemos que desarrollamos un programa que realiza las siguientes tareas con un
fichero de audio:

1. Abre el fichero (parece un pato).

2. Lee la señal que contiene (nada como un pato).

3. Realiza algún tipo de procesado con la señal.

4. Cierra el fichero (hace cuac como un pato).

100
6.3. POLIMORFISMO, HERENCIA Y TIPADO PATOSO.

El tipado patoso permite adaptar con pocos cambios el programa al procesado del audio de un vídeo
colgado de una página de YouTube, siempre que tengamos funciones que:

Parezcan un pato (accedan al vídeo desde su página web).

Naden como un pato (extraigan la señal de audio del vídeo).

Hagan cuac como un pato (cierren la conexión a la página web).

Del mismo modo, podríamos acceder a la tarjeta de sonido del ordenador, o recibir el fichero por email.
Sin embargo, a veces el tipado patoso puede resultar incluso peligroso. Es el rovelló typing:

Si parece un rovelló, crece en la hojarasca de un pinar y sabe a rovelló, pero


te mueres después de comerlo, entonces sin duda no era un rovelló.

Por ejemplo, hemos visto que no éramos capaces de sumar un vector de números y una cadena de
texto... pero no todo es imposible (y el resultado no es el esperado):

>>> Vector(('1', '2')) + ('3', '4')


Vector(['13', '24'])

Aunque, y esta es una de las características fundamentales del duck typing, quién sabe si esto era
justamente lo que queríamos...

Ejercicio: multiplicación de vectores y ortogonalidad.


Añada al fichero algebra/[Link] los métodos sobrecargados necesarios para implementar
las operaciones con vectores siguientes. Cada método deberá incluir una cadena de documen-
tación que explique su funcionamiento y que incluya las pruebas unitarias indicadas usando la
biblioteca doctest, según lo explicado en el apartado 5.3.1.

Multiplicación:
Sobrecargue el operador asterisco (*, métodos __mul__(), __rmul__(), etc.) para imple-
mentar la multiplicación de un vector por una constante u otro vector.

Se entiende la multiplicación de dos vectores como el vector cuyos elementos son el


producto de los elementos de los dos argumentos:

[1, 2, 3] ∗ [4, 5, 6] = [4, 10, 18]

Pruebas unitarias usando doctest:

"""
>>> v1 = Vector([1, 2, 3])
>>> v2 = Vector([4, 5, 6])
>>> v1 * 2
Vector([2, 4, 6])
>>> v1 * v2
Vector([4, 10, 18])
"""

101
CAPÍTULO 6. PROGRAMACIÓN ORIENTADA A OBJETO.

Producto escalar:
Sobrecargue el operador arroba (@, métodos __matmul__(), __rmatmul__(), etc.) para
implementar el producto escalar de dos vectores:

[1, 2, 3] @ [4, 5, 6] = 32

Prueba unitaria:

"""
>>> v1 = Vector([1, 2, 3])
>>> v2 = Vector([4, 5, 6])
>>> v1 @ v2
32
"""

Descomposición tangencial-normal:

Dados dos vectores v1 y v2 , es posible descomponer v1 en dos componentes, v1 = v1 +v1⊥

tales que v1 es tangencial (paralela) a v2 , y v1⊥ es normal (perpendicular) a v2 .

Se puede demostrar:
∥ v1 · v2
v1 = v2
|v2 |2

v1⊥ = v1 − v1

Sobrecargue el operador doble barra inclinadaa (//, métodos __floordiv__(),


__rfloordiv__(), etc.) para que proporcione la componente tangencial, y el operador
tanto por ciento ( %, métodos __mod__(), __rmod__(), etc.) para que proporcione la com-
ponente normal.
Pruebas unitarias (puede colocar la prueba unitaria de cada operador en su correspon-
diente método o agrupar las dos en uno de ellos):

"""
>>> v1 = Vector([2, 1, 2])
>>> v2 = Vector([0.5, 1, 0.5])
>>> v1 // v2
Vector([1.0, 2.0, 1.0])
>>> v1 % v2
Vector([1.0, -1.0, 1.0])
"""

El fichero deberá empezar con una cadena de texto de documentación que incluya el nombre y
apellidos del integrante o integrantes del grupo de prácticas.

Las funciones deberán contar con su correspondiente cadena de texto de documentación en las
que se incluirán las pruebas unitarias indicadas, que deberán pasarse con éxito al ejecutar la
función [Link]().

Se valorará lo pythónico de la solución; en concreto, su claridad y sencillez. También se tendrá


en cuenta el cumplimiento de las reglas de PEP-ocho.

102
6.3. POLIMORFISMO, HERENCIA Y TIPADO PATOSO.

a
Es discutible esta elección de sobrecarga, dado que extraer la componente tangencial no es equivalente a
ningún tipo de división. Sin embargo, está justificado en el hecho de que su representación matemática es dos
barras paralelas (∥), similares a las usadas para la división entera (//).

Por otro lado, y de manera parecida (aunque no idéntica) al caso de la división entera, las dos componentes
cumplen: v1 = v1 // v2 + v1 % v2.

103
CAPÍTULO 6. PROGRAMACIÓN ORIENTADA A OBJETO.

104
Capítulo 7

Programación funcional.

En programación se distinguen distintos paradigmas de programación. Un paradigma es un modo de


clasificar los lenguajes de programación atendiendo a sus características. La definición no está muy
clara, y muchas veces es difícil asignar un lenguaje a un paradigma u otro:

No sé cómo decirlo, pero sé lo que es cuando lo veo...

La principal división entre lenguajes de programación los separa entre imperativos y declarativos:

Programación imperativa:
El programador indica qué instrucciones debe seguir el programa para alcanzar el resultado.
Programación procedimental, que agrupa las instrucciones en subrutinas (también llamadas,
de forma menos precisa, funciones). Ejemplos típicos de programación procedimental son el
C, el Pascal, el Fortran, etc.
Programación orientada a objeto, en la que las instrucciones se agrupan entorno a objetos
que contienen tanto el estado del programa como los métodos para modifcarlo. Los más
extendidos son el C++ y el Java, que, a su vez, ocupan una posición dominante dentro del
amplio espectro de los lenguajes de programación (con permiso de Python, que, como se vio
en el capítulo anterior, es de los pocos lenguajes de programación orientada a objeto puro).
Programación declarativa:
El programador sólo indica las propiedades de las condiciones de partida y del resultado deseado,
pero no cómo lograr este resultado.
En la programación lógica se declaran una serie de propiedades y reglas, y luego es posible
realizar consultas al programa (hay un ejemplo, usando Prolog, más adelante).
En la programación matemática, el ¿programador? plantea un problema matemático, y el
programa lo resuelve. Es el caso de Mathematica o Maple.
En programación funcional el resultado deseado es el producto de aplicar sucesivamente una
serie de funciones (en el sentido funcional del término). Dos de los más populares son Erlang
y Haskell, aunque continúa siendo un paradigma muy minoritario.

7.1. Introducción a la programación funcional.

7.1.1. Programación declarativa vs. imperativa.

La programación declarativa puede resultar un tanto marciana para quien sólo ha trabajado en pro-
gramación imperativa. Y eso abarca al 90 % de los programadores del mundo; el 99 %, si excluimos a
los que han estudiado la carrera de Informática o de Ciencia de Computadores.

105
CAPÍTULO 7. PROGRAMACIÓN FUNCIONAL.

Pequeño ejemplo de programación lógica usando Prolog.

Para ilustrar la gran diferencia que existe entre el paradigma imperativo y el declarativo, vamos a
escribir un pequeño programa usando Prolog , el estándar en programación lógica.
En Prolog, primero hay que declarar las reglas1 :

1 esHijo(anna, pedro) :- true.


2 esHijo(david, pedro) :- true.
3
4 esHijo(X, Y) :- esPadre(Y, X).
5
6 esPadre(pedro, juan) :- true.
7 esPadre(pedro, victor) :- true.
8
9 esHermano(X, Y) :- esHijo(X, Z) , esHijo(Y, Z) , X \= Y.

Fijémonos en que no se hace ninguna definición de lo que es esHijo, esPadre o esHermano; simplemente
las usamos. Tampoco se hace ninguna declaración de qué significa pedro, anna o los hermanos de ésta.
Una vez definidas las reglas, podemos hacer distintas preguntas a Prolog:

?- esHermano(anna, david).
yes

?- findall(X, esHermano(juan, X), List).


List = [victor,anna,david]

?- findall(X, esHijo(X, pedro), List).


List = [juan,victor,anna,david]

Prolog ha tenido un impacto reducido en la industria, pero no nulo. Y, aunque pueda parecer mentira,
existen aplicaciones escritas en Prolog de varios cientos de miles de líneas de código.

7.1.2. El paradigma de programación funcional.

La programación funcional es un paradigma de la programación declarativa que tiene sus orígenes en


el cálculo lambda desarrollado por por Alonzo Church en los años 1930. En él se trata la computación
como la evaluación de funciones matemáticas, evitando la mutabilidad y el cambio del estado interno
del programa.
Como su nombre indica, la programación funcional se basa en el uso de funciones, pero, en este caso,
de lo que se denomina funciones puras. Una función pura es aquélla que lo único que hace es devolver
uno o varios valores a uno o varios argumentos de entrada, y cuya salida a una entrada determinada
es siempre la misma. Una implicación de esto es que una función pura no puede modificar el estado
interno del programa, ni tener efectos colaterales.
El paradigma resulta en soluciones completamente distintas a las de la programación imperativa. Por
ejemplo, en esta última es habitual usar asignaciones del tipo a = 12. En un lenguaje imperativo, como
C o Java, esta expresión es una orden que le indica al programa que debe almacenar el valor 12 en la
posición de memoria reservada para la variable a. Esta acción, en sí misma, representa una alteración
del estado interno del programa y está, por tanto, prohibida en programación funcional.
En programación funcional sí pueden existir asignaciones, aunque hay lenguajes en los que no existen
de ningún tipo. En los que hay algo parecido, por ejemplo, en Haskell, una expresión del tipo a =
1
Existen distintas implementaciones de Prolog que permiten su ejecución online. Una de las más usadas es
[Link]

106
7.1. INTRODUCCIÓN A LA PROGRAMACIÓN FUNCIONAL.

12 sólo permite asignar un nombre a un valor o resultado de una computación anterior, y, una vez
asignado, éste no puede variar nunca más (si hemos dicho que a vale 12, es un absurdo decir después
que a vale otra cosa). En otros, como Erlang, sí es posible cambiar el valor asignado a un cierto nombre,
pero eso se considera impuro por parte de los puristas de la programación funcional, y se efectúa de
manera semejante a cómo se se hace en Python al asignar un valor inmutable a un nombre: primero
se destruye la asignación previa y, a continuación, se crea una nueva asignación con el nuevo valor.
Por otro lado, en programación imperativa existen subrutinas que pueden recordar a las funciones
puras, pero que, o bien no devuelven siempre lo mismo, o bien hacen cosas distintas además de pro-
porcionar una misma salida para una misma entrada. Un ejemplo de lo primero sería la programación
orientada a objeto, en la que los métodos de un determinado objeto sirven para modificar el estado
interno del mismo. Un caso en el que se hacen cosas distintas aparte de devolver un valor es el de
cualquier operación de entrada/salida. En éstas, el valor retornado por la función suele ser irrelevante,
y lo realmente importante es lo que se lee o escribe en el terminal o en un fichero.
Planteado inicialmente como una idea teórica con poca proyección práctica, a mediados de los años
80 experimentó un fuerte crecimiento gracias a la aparición de lenguajes como el Miranda , de carácter
propietario, y el Haskell , inspirado en el primero, pero de carácter libre y código abierto. De todos
modos, es el éxito de Erlang el que puso la programación funcional en la cresta de la ola de los
paradigmas de programación.

Una pequeña historia de Erlang

El nombre de Erlang hace referencia tanto al matemático danés Agner Krarup Erlang , pionero de la
ingeniería del tráfico y la teoría de colas, como a la empresa en la que se desarrolló, la compañía
telefónica sueca Ericsson (Ericsson Language). En los años 80 del siglo XX, Ericsson necesitaba una
plataforma, robusta y tolerante a fallos, que permitiera la implantación de la inminente telefonía móvil
digital. Para ello contrataron a un grupo de ingenieros comandados por el inglés Joe Armstrong . Éstos
diseñaron e implementaron Erlang. Un lenguaje que, como dice su eslogan, ”se compila una vez y se
ejecuta para siempre” 2 .
El sistema desarrollado por el equipo de Armstrong fue un éxito rotundo desde el punto de vista
técnico, pero tenía un problema gravísimo para los accionistas propietarios de Ericsson: no había
técnicos que supieran programar en Erlang, por lo que se veían a formarlos en la propia compañía.
Ahora bien, los pocos que conseguían dominar el lenguaje exigían una remuneración acorde con sus
conocimientos. Así que el consejo de administración de Ericsson decidió, literalmente, prohibir el uso
de lenguajes de programación que no fueran de código abierto, y, en la práctica, echaron a Armstrong.
El resultado de estas operaciones fue un completo desastre para Ericsson, con problemas técnicos
continuos hasta que decidieron dos cosas: hacer Erlang de dominio público, y suplicar a Armstrong
que volviera a la empresa. Cuando éste preguntó si podría continuar trabajando en Erlang, la respuesta
fue: ”tú encargate de los problemas técnicos, que nosotros nos encargaremos del dinero; y si tú no nos
dices nada, tampoco nosotros preguntaremos”.
Hoy Erlang es el lenguaje de referencia, no sólo en Ericsson, sino en un montón de compañías telefónicas
más. También es el principal lenguaje en el que está implementado WhatsApp y, junto con su pariente
cercano Elixir , es una de las opciones más adecuadas para manejar aplicaciones de paralelismo masivo
(millones de procesos concurrentes), como los juegos multijugador masivos online ( MMOG ).

7.1.3. Características y alicientes de la programación funcional.

Entre las características principales de la programación funcional hay que resaltar las siguientes:
2
El eslogan es un coña del de Java, que es ”se compila una vez y se ejecuta en todas partes”.

107
CAPÍTULO 7. PROGRAMACIÓN FUNCIONAL.

Pasión por la inmutabilidad. En programación funcional no existen variables propiamente dichas.


Existen nombres asociados a valores. Una vez asignado un nombre a un valor, éste no debería
cambiar nunca.

• Esta es una característica disponible en buena parte de los tipos y estructuras estándar de
Python, como los tipos simples o las tuplas y los conjuntos congelados.

Ausencia de efectos colaterales e inmutabilidad del estado interno del programa. Aunque estos
efectos son inevitables, en un punto u otro de un programa que cualquier cosa útil, los lenguajes
funcionales obligan a identificar y aislar los puntos donde esto ocurre.

Aversión a los bucles, especialmente los basados en contadores, ya que, por su propia naturaleza,
implican el uso de valores mutables.

• Python proporciona los mecanismos clásicos de iteración, pero también soluciones propias
de la programación funcional, como los iteradores y las comprensiones.

Preferencia del uso de la recursión frente al de la iteración.

• La recursión es una técnica adorada por matemáticos e informáticos, pero que suele gozar
de mala prensa entre los ingenieros.
• Parte de los motivos de esta mala prensa son el sobrecoste computacional y un uso intensivo
de la pila, que puede, incluso, llegar a provocar su desbordamiento (stack overflow).
• Sin embargo, la mayor parte de los lenguajes funcionales implementan optimización de
llamada al final , que evita este uso de la pila, haciendo la recursión tan eficiente como la
iteración.
◦ En Python no se realiza optimización de llamada al final, con lo que la recursión es
muy ineficiente.

Uso de las funciones como objetos de primera clase. Esto quiere decir que una función puede ser
asignada, usada como argumento de otra función o como valor de retorno de la misma.

• En Python las funciones son objetos normales y corrientes, como los de cualquier otro tipo
estándar o clase definida por el usuario, y pueden ser usadas en cualquier posición en los
que éstos pueden serlo.

Tendencia al uso de evaluación perezosa, consistente en que un cálculo no se lleva a cabo en


tanto su resultado no sea necesario para realizar otro cálculo.

• Python realiza evaluación estricta, pero, a su vez, admite estructuras típicas de la evaluación
perezosa, como los objetos de longitud infinita.

Las principales ventajas de la programación funcional son:

Mayor legibilidad e inteligibilidad de las funciones puras, ya que no tienen efectos colaterales y,
en general, realizan pocas tareas (idealmente, sólo una) y muy concretas.

• Aunque la sintaxis de los lenguajes funcionales puede resultar enrevesada para el usuario
acostumbrado a los lenguajes imperativos.

Mayor facilidad para comprender la estructura global de un programa, ya que éstos se escriben
en lenguajes de nivel superior a los escritos en programación imperativa.

Verificabilidad: al menos en teoría, es posible demostrar si un programa que sólo emplee funciones
puras va a funcionar correctamente sin necesidad de ejecutar todos los escenarios posibles.

108
7.2. ELEMENTOS DE PROGRAMACIÓN FUNCIONAL EN PYTHON.

Modularidad y reusabilidad. La propia naturaleza del paradigma obliga a descomponer los pro-
blemas en otros de menor tamaño, resueltos con funciones, forzosamente elementales, que pueden
ser usadas para múltiples cometidos.

Facilidad de la depuración y prueba: como no hay efectos colaterales, si se puede demostrar el


correcto funcionamiento de cada función, se demuestra también el del programa completo.

Concurrencia: el uso de objetos inmutables y la ausencia de efectos colaterales facilita la ejecución


de un número prácticamente ilimitado de proceso concurrentes sin peligro de colisión.

Tolerancia a fallos, obtenida mediante la capacidad de aislar partes de los programas sin afectar
al resto, gracias al uso de datos inmutables y la ausencia de efectos colaterales.

7.2. Elementos de programación funcional en Python.

Python no es un lenguaje de programación funcional pura, y carece de muchas de las características


que definen a estos. Por ejemplo, no implementa optimización de llamada al final o evaluación perezosa.
Sin embargo, sí dispone de muchas herramientas típicas de la programación funcional. De hecho, y
aunque Python es fundamentalmente un lenguaje orientado a objeto (la némesis de la programación
funcional), todo el lenguaje desprende un cierto aroma funcional, particularmente al usar las soluciones
consideradas más pythónicas.
No obstante, es imposible construir un auténtico programa funcional en Python porque no garantiza
dos cosas: la pureza de las funciones y la inmutabilidad del estado interno. Aunque nosotros escri-
bamos el código de manera cuidadosa para que ambos requisitos se cumplan, el compilador no tiene
mecanismos para saber que efectivamente lo hacen, y no podrá aplicar las técnicas de optimización
características de la programación funcional. A pesar de todo, sí vale la pena esforzarse en progra-
mar de la manera más funcional posible, ya que el programa sí tendrá muchas de las características
deseables del paradigma.
Entre las herramientas típicas de la programación funcional disponibles en Python, se pueden destacar
las siguientes, que, en todos los casos, fueron introducidas previamente en Erlang, Haskell, o algún
otro lenguaje funcional.

Comprensiones.

Iteradores y generadores.

Funciones anónimas o lambda.

Funciones de orden superior.

Decoradores.

Alguna de estas herramientas ya han sido vistas con anterioridad. En concreto, ya vimos en la Sec-
ción 4.4 las comprensiones de listas, conjuntos y diccionarios. En las secciones siguientes se hace una
presentación del resto.

7.2.1. Iteradores y generadores.

En programación funcional, los bucles están proscritos. De hecho, (casi) ninguno dispone de una
estructura del estilo del bucle for, y pocos disponen de algo semejante al while. En su lugar, la
programación funcional favorece el uso de las funciones recursivas, la comprensiones y los iteradores.

109
CAPÍTULO 7. PROGRAMACIÓN FUNCIONAL.

Clases iterables.

Como se ha visto con anterioridad, un iterador es un objeto que permite recorrer un iterable. A su
vez, un objeto compuesto es un iterable si somos capaces de recorrer sus elementos de uno en uno.
Esto incluye todos los tipos compuestos estándar de Python, como las listas, tuplas o diccionarios, así
como cualquier objeto definido por el usuario que incorpore el método __getitem__().
En otros objetos no tiene sentido el método __getitem__() porque no son ordinalizables, como sí lo son
las listas, las tuplas o los vectores definidos en el capítulo anterior, ni son accesibles por clave, como
lo son los diccionarios. Es el caso, por ejemplo, de los conjuntos. En estos casos es posible convertir
el objeto en iterable definiendo los métodos __iter__(), que inicializa la iteración sobre los elementos
del objeto, y __next__(), que accede al elemento siguiente, o eleva la excepción StopIteration en caso
de que no queden elementos:

>>> class ParesChiquitos:


... def __iter__(self):
... [Link] = 0
... return self
... def __next__(self):
... [Link] += 2
... if [Link] > 10: raise StopIteration
... return [Link]
...

>>> pares = ParesChiquitos()

>>> list(pares)
[2, 4, 6, 8, 10]

Esta clase sencilla ilustra, ya de paso, una característica muy reseñable de la programación funcional:
su capacidad de manejar objetos de longitud infinita. Así, si eliminamos la condición [Link] > 10, que
eleva la excepción StopIteration cuando se supera el número 10, la clase producirá toda la secuencia
infinita de números pares3 .

Generadores.

Cuando no estamos interesados en la propia clase, sino sólo en el iterador, una alternativa muy cómoda
es el uso de generadores . Un generador es una función que devuelve un iterador que permite recorrer
los valores generados por la función. Para ello, ésta debe usar la cláusula yield como alternativa de
la habitual return. De hecho, esta última provocará la elevación de la excepción StopIteration. Toda
función que incluya en su definición una cláusula yield, aunque no llegue a ejecutarse nunca, es un
generador.
Al ejecutar el generador, la función devuelve un iterador y continúa su ejecución hasta que alcanza
una cláusula yield. En ese punto, el generador se para y espera a que la función que creó el iterador
ejecute next() sobre éste. Cuando esto ocurre, el generador entrega el argumento de la cláusula yield
como valor de retorno de next() y continúa su ejecución hasta encontrar otra cláusula yield, en cuyo
caso el proceso vuelve a empezar, o una return, que eleva la excepción StopIteration y finaliza la
iteración.
El útil ejemplo anterior, usando una función generadora, es:

>>> def paresChiquitos():


... par = 0
... while True:

3
En ese caso, ya de paso, sería conveniente cambiar el nombre de la clase de ParesChiquitos a Pares a secas.

110
7.2. ELEMENTOS DE PROGRAMACIÓN FUNCIONAL EN PYTHON.

... par += 2
... if par > 10: return
... yield par
...

>>> pares = paresChiquitos()

>>> list(pares)
[2, 4, 6, 8, 10]

Los generadores tienen una ventaja adicional: permiten la comunicación bidireccional. Esto se consigue
con el método send(). La función invocadora ejecuta el método send() con el objeto que desea enviar
al generador, que lo recibe como resultado de la cláusula yield, que debe escribirse entre paréntesis.
Sólo se pueden enviar objetos a un iterador que ya haya devuelto algún valor con su cláusula yield.
Podemos usar send() para, por ejemplo, reiniciar el iterador a un valor distinto4 :

>>> def paresChiquitos():


... par = 0
... while True:
... par += 2
... if par > 10: return 'No quedan más pares chiquitos'
... ini = (yield par)
... if ini: par = ini
...

>>> pares = paresChiquitos()

>>> next(pares)
2

>>> next(pares)
4

>>> [Link](5)
7

>>> next(pares)
9

>>> next(pares)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration: No quedan más pares chiquitos

Un último interés de las funciones generadoras es que proporcionan un mecanismo sencillo para la
ejecución multiproceso en paralelo: cuando yield envía un valor al next(), tanto la función generadora
como la función invocadora pueden proseguir su ejecución con independencia la una de la otra. En
sistemas que lo permita, esto permite la ejecución de los dos procesos en paralelo. De hecho, los
generadores fueron el primer mecanismo para realizar multiproceso en Python, y constituyen la base
de los mecanismos actuales.

Expresiones generadoras.

Una última opción para generar iteradores de manera sencilla son las expresiones generadoras . Se trata
en esencia, de una comprensión en la que se usan paréntesis como caracteres delimitadores:

4
Aunque, como veremos, podemos cargarnos el significado profundo de tan útil función.

111
CAPÍTULO 7. PROGRAMACIÓN FUNCIONAL.

>>> paresChiquitos = (par for par in range(2, 11, 2))

>>> next(paresChiquitos)
2

>>> next(paresChiquitos)
4

>>> next(paresChiquitos)
6

Acerca de la pureza de la función next().


Resulta paradójica la afición de la comunidad funcional a los iterables y la función next(). Si
una función sólo es considerada pura cuando siempre devuelve el mismo valor para la misma
entrada, un claro ejemplo de función que devuelve valores distintos en cada invocación es la
función next().

La paradoja suscita arduos debates en la comunidad funcional, con unos defendiendo que next()
es impura y otros que es la esencia misma de la programación funcional. Como síntesis benévola,
puede decirse que next() no es una función, sino un operador, así que su pureza no es un asunto
de especial relevancia. A fin de cuentas, hasta los funcionales más conspicuos necesitan algunas
veces acceder secuencialmente a los datos.

7.2.2. Funciones anónimas, la cláusula lambda.

La base de la programación funcional es el cálculo lambda desarrollado en los años 30 y 40 del siglo
XX con el objetivo de desarrollar un sistema formal para expresar la computación. Como su nombre
indica, el cálculo lambda se basa en las funciones lambda que, en esencia, son funciones anónimas.
El comando lambda toma un conjunto de argumentos y una expresión que los contiene y devuelve una
referencia a función que devuelve la evaluación de la expresión.
Su sintaxis es:
lambda argumentos: expresión

Por ejemplo, la siguiente función anónima puede ser útil para sumar pares de números:

>>> lambda x, y: x + y
<function <lambda> at 0x7f9e7c6c7488>

>>> _(2, 3)
5

Existen varios motivos por los que nos puede interesar usar funciones anónimas. Por ejemplo, la
aplicación parcial , o congelación de argumentos, consistente en transformar una función que toma
varios argumentos en otra con un número menor de los mismos:

>>> bonito = lambda *args, **dicc: print('Que bonito es', *args, '... ¿o no?', **dicc)

>>> bonito('Sabadell', 'Rubí', 'Manresa', sep=', ')


Que bonito es, Sabadell, Rubí, Manresa, ... ¿o no?

112
7.2. ELEMENTOS DE PROGRAMACIÓN FUNCIONAL EN PYTHON.

También veremos casos, en los apartados siguientes, en los que es necesario pasar una función como
argumento a otra. En muchas ocasiones, la función pasada como argumento es una expresión simple
que no justifica la creación de una función estándar.
Sin embargo, son muchos los que no ven con buenos ojos las funciones anónimas. Sobre todo, porque en
Python es posible definir una función en cualquier punto del código. Por ejemplo, Fredrik Lundh , uno
de los programadores estrella del pythoniverso, propone los pasos siguientes para usar correctamente
las funciones anónimas:

1. Escribe una función anónima.

2. Escribe un comentario que explique lo que hace.

3. Piensa en un nombre que sintetice lo que dice el comentario.

4. Convierte la función anónima en una con ese nombre.

5. Borra el comentario.

Aplicando estos pasos a la función bonito(), se obtiene una expresión tan simple o más que la obtenida
usando funciones anónimas:

>>> def bonito(*args, **dicc): print('Que bonito es', *args, '... ¿o no?', **dicc)

>>> bonito('Vacarisses', 'Sant Quirze', sep=', ')


Que bonito es, Vacarisses, Sant Quirze, ... ¿o no?

Donde no hay discusión es en que usar una función anónima para asignarla a un nombre es com-
pletamente absurdo. Así que esta segunda opción para la función bonito() es mucho más bonita.
No obstante, veremos a continuación casos en los que las funciones anónimas simplifican el código y
aumentan su claridad y legibilidad.

7.2.3. Funciones de orden superior.

Una función de orden superior es una función que toma por argumento o devuelve otra función. Esta
es una característica fundamental en programación funcional, permitiendo, por ejemplo, realizar cosas
que en programación imperativa requeriría el uso de los denostados bucles.
En Python existen varias funciones que toman otras como argumentos. Algunas de ellas son de suma
importancia, pero hay otras que pueden, y seguramente deben, ser sustituidas por comprensiones en
muchas ocasiones. No obstante, dada su importancia en programación funcional y lo extendido de su
empleo en Python y otros lenguajes, se describen todas ellas en los apartados siguientes.

[Link]. Mapeos: la función map().

La función map() toma por argumentos una función y una serie de iterables:

map(función, iterable, ...)

Debe proporcionarse tantos iterables como argumentos tome la función, y map() devuelve un iterador
que recorre los valores func(iter1[0], iter2[0], ...), func(iter1[1], iter2[1], ...), etc.

113
CAPÍTULO 7. PROGRAMACIÓN FUNCIONAL.

>>> NUMEROS = map(lambda s: [Link](), ('uno', 'dos', 'tres'))

>>> next(NUMEROS)
'UNO'

>>> next(NUMEROS)
'DOS'

Esta misma funcionalidad puede obtenerse, de manera más clara, usando una expresión generadora:

>>> NUMEROS = ([Link]() for s in ('uno', 'dos', 'tres'))

>>> list(NUMEROS)
['UNO', 'DOS', 'TRES']

[Link]. Filtrados: la función filter().

La función filter() toma por argumentos una función y un iterable:

filter(función, iterable)

A la salida, filter() devuelve los elementos del iterable para los que la función evalúa a True.

>>> pares = filter(lambda x: not x % 2, range(10))

>>> next(pares)
0

>>> list(pares)
[2, 4, 6, 8]

Nuevamente, es posible realizar lo mismo, e incluso más, con una expresión generadora u otro tipo de
comprensión.

[Link]. Reducciones: la función reduce().

La función reduce() forma, junto con map() y filter(), la santísima trinidad de las funciones de orden
superior en programación funcional. Sin embargo, en las versiones recientes de Python, reduce() ha
sido postergada a la biblioteca functools. Por este motivo, para poderla usar es necesario importarla
primero con la orden from functools import reduce.
Su sintaxis básica es:

reduce(función, iterable, inicial=None)

reduce() aplica acumulativamente la función, que debe tomar dos valores de entrada y devolver uno de
salida, a los elementos del iterable, que ahora no puede ser de longitud infinita, y devuelve el resultado
final de la operación. En primera instancia, reduce() toma los dos primeros valores del iterable y
calcula función(iter[0], iter[1]); a continuación, toma el resultado y el tercer elemento y calcula
función(resultado1, iter[2]); y así sucesivamente, hasta obtener:

114
7.2. ELEMENTOS DE PROGRAMACIÓN FUNCIONAL EN PYTHON.

función(... (función(función(iter[0], iter[1]), iter[2]), ...), iter[N-1])}

Un ejemplo de uso de reduce() es el cálculo del factorial de un número entero:

>>> from functools import reduce

>>> factorial = lambda n: reduce(lambda x, y: x * y, range(2, n + 1))

>>> factorial(5)
120

>>> dict(zip(range(2, 10), map(factorial, range(2, 10))))


{2: 2, 3: 6, 4: 24, 5: 120, 6: 720, 7: 5040, 8: 40320, 9: 362880}

115
CAPÍTULO 7. PROGRAMACIÓN FUNCIONAL.

Iteradores y generadores: generación de números aleatorios.

Generación de números aleatorios.


El algoritmo de generación lineal congruente ( LGC ), permite generar secuencias pseudo-
aleatorias de características controladas. Se basa en aplicar iterativamente la fórmula:
xn+1 = (axn + c) mód m
Donde se denomina módulo a m > 0, multiplicador a 0 < a < m, incremento a 0 ≤ c < m,
y 0 ≤ x0 < m es el valor inicial, o semilla, de la secuencia aleatoria generada. La secuencia
es periódica, ya que, cada vez que repetimos un valor de xn , volvemos a generar la misma
otra vez. Los valores generados cumplen 0 ≤ xn < m; por tanto, la secuencia más larga
posible es de longitud m, en cuyo caso, cada valor 0 ≤ xn < m es producido una sola vez.
El módulo m suele tomar como valor una potencia entera de dos para facilitar el cálculo
del resto de la división entera mediante el operador desplazamiento de bits. Una elección
adecuada del incremento c y el multiplicador a permite que la secuencia generada tenga
el periodo máximo, igual a m:
m y c no deben tener factores primos en común.
a − 1 debe ser divisible por todos los factores primos de m (aunque no mucho).
Si m es divisible por 4, a − 1 también debe serlo, pero no por 8.
Por ejemplo, el generador aleatorio del estándar POSIX usa los valores siguientes:
m = 248
a = 25214903917
c = 11

1. Escriba la clase Aleat que implemente un generador de números aleatorios en el rango


0 ≤ xn < m usando el método LGC con las características siguientes:

Los objetos de la clase serán iteradores, para lo que habrá de definirse el método
mágico __next__(), que será el que efectuará la generación en sí misma y deberá
devolver el número aleatorio siguiente.
Los valores de m, a y c y la semilla x0 deben ser configurables al crear el objeto (argu-
mentos opcionales del método mágico __init__()). Los cuatro argumentos opcionales
serán indicados, obligatoriamente, por clave (no pueden ser posicionales).
Por defecto, los valores de m, a y c serán los usados por el estándar POSIX. El de la
semilla será x0=1212121.
El método __call__() , que sobrecarga la llamada a función, es decir, el uso del objeto
como si fuera una función con sus argumentos entre paréntesis, se usará para reiniciar
la secuencia con la semilla indicada en su único argumento.
La cadena de documentación de la clase deberá incluir las siguientes pruebas unitarias
a ejecutar con la biblioteca doctest:

116
7.2. ELEMENTOS DE PROGRAMACIÓN FUNCIONAL EN PYTHON.

>>> rand = Aleat(m=32, a=9, c=13, x0=11)


>>> for _ in range(4):
... print(next(rand))
...
16
29
18
15

>>> rand(29)
>>> for _ in range(4):
... print(next(rand))
...
18
15
20
1

2. Escriba la función generadora aleat() que implemente el mismo generador de números


aleatorios en el rango 0 ≤ xn < m que en el punto anterior.

Los valores de m, a y c y la semilla deben ser configurables al crear la función, y


tendrán los mismos valores por defecto que en el caso de la clase Aleat.
En caso de enviársele un valor al generador, con su método send(), éste debe reiniciar
la secuencia tomando el argumento como semilla de la nueva secuencia.
La cadena de documentación de la clase deberá incluir las siguientes pruebas unitarias
a ejecutar con la biblioteca doctest:

>>> rand = aleat(m=64, a=5, c=46, x0=36)


>>> for _ in range(4):
... print(next(rand))
...
34
24
38
44

>>> [Link](24)
38
>>> for _ in range(4):
... print(next(rand))
...
44
10
32
14

Incluya los códigos fuente solicitados en el fichero [Link] y súbalo a la tarea de Atenea
habilitada al efecto. Recuerde colocar un mensaje de documentación al inicio del fichero que
incluya el nombre y apellidos del autor.

117
CAPÍTULO 7. PROGRAMACIÓN FUNCIONAL.

[Link]. Ordenación: la función sorted().

Una tarea muy habitual en programación consiste en ordenar los elementos de un objeto compuesto.
La función que realiza esta misión en Python es sorted() , cuya sintaxis es:
sorted(iterable, /, *, key=None, reverse=False)

En principio, si no se proporcionan los argumentos forzosamente por clave key y reverse, sorted devuel-
ve los elementos del argumento exclusivamente posicional iterable ordenados de manera ascendente.
Para ello, sorted usa el método __lt__() de manera estable; esto es: sólo se altera la posición relativa
de dos elementos si el resultado de la operación primero <= segundo es False.

>>> sorted((13, 1, 5, 3, 17, 2))


[1, 2, 3, 5, 13, 17]

A menudo, los objetos definidos por el usuario no implementan la comparación __lt__(). En ese caso,
podemos usar el argumento key para pasar a sorted() una función que devuelva un valor para el que
sí esté definida la comparación.
Por ejemplo, la clase Nota almacena el nombre del alumno y su nota, pero no tiene sentido que defina
la comparación:

>>> class Alumno:


... def __init__(self, nombre, nota):
... [Link] = nombre
... [Link] = nota
...
... def __repr__(self):
... return repr([Link])

Usando funciones anónimas, podemos ordenar una lista de alumnos según su nombre o su nota:

>>> alumnos = [Alumno('Eric', 8), Alumno('David', 7), Alumno('Berta', 9), Alumno('Carla', 7)]

>>> sorted(alumnos, key=lambda alumno: [Link])


['David', 'Carla', 'Eric', 'Berta']

>>> sorted(alumnos, key=lambda alumno: [Link])


['Berta', 'Carla', 'David', 'Eric']

Ordenación según el nombre del atributo.

El carácter interpretado de Python y el hecho de que un objeto es, básicamente, un espacio de nombres
permite operaciones que resultarían imposibles en la mayor parte de lenguajes. En concreto, las clases
y objetos de Python soporta el método __getattribute__(), que devuelve el valor de un cierto atributo
del objeto a partir de su nombre almacenado en una cadena de texto.
Por ejemplo, podemos acceder al nombre o la nota de los alumnos del ejemplo anterior:

>>> for alumno in alumnos:


... print(alumno.__getattribute__('nombre'), alumno.__getattribute__('nota'))
...
Eric 8
David 7
Berta 9
Carla 7

118
7.2. ELEMENTOS DE PROGRAMACIÓN FUNCIONAL EN PYTHON.

La operación vista más arriba, de ordenar un iterable de objetos según el valor de uno de sus atributos,
es lo suficientemente general como para definir una función que lo implemente con independencia de
la clase de los objetos y del nombre del atributo:

>>> def ordenaAtributo(objetos, atributo):


... return sorted(objetos, key=lambda objeto: objeto.__getattribute__(atributo))

Podemos usarla para ordenar los alumnos, los coches de la familia, o cualquier otra cosa:

>>> class Coche:


... def __init__(self, marca, modelo, color):
... [Link] = marca
... [Link] = modelo
... [Link] = color
...
... def __repr__(self):
... return ' '.join(([Link], [Link], [Link]))
...

>>> coches = [Coche('Bugatti', 'Veyron', 'negro'), Coche('Jaguar', 'XKR', 'verde'),


... Coche('Ferrari', 'Enzo', 'rojo'), Coche('Porsche', 'Carrera', 'gris')]

>>> ordenaAtributo(coches, 'marca')


[Bugatti Veyron negro, Ferrari Enzo rojo, Jaguar XKR verde, Porsche Carrera gris]

>>> ordenaAtributo(coches, 'modelo')


[Porsche Carrera gris, Ferrari Enzo rojo, Bugatti Veyron negro, Jaguar XKR verde]

>>> ordenaAtributo(coches, 'color')


[Porsche Carrera gris, Bugatti Veyron negro, Ferrari Enzo rojo, Jaguar XKR verde]

7.2.4. Decoración de funciones.

Este tema es suplementario y no se evalúa.

En Python, una función también puede devolver otra función. A menudo, esto se realiza para añadir o
modificar las funcionalidades de la función. A esta operación se la denomina decoración de la función.
El proceso de decoración de una función involucra siempre, como mínimo, otras dos funciones: una
que hace de envoltorio de la función decorada, y otra que le aplica el envoltorio y devuelve la función
decorada.
Un ejemplo mínimo de decoración, que, por cierto, no decora nada, es:

>>> def decora(función):


... def envoltorio(*args, **dicc):
... return función(*args, **dicc)
... return envoltorio

Al ejecutar decora con función como argumento, se obtiene una copia de la función envoltorio que
todo lo que hace es ejecutar la función:

119
CAPÍTULO 7. PROGRAMACIÓN FUNCIONAL.

>>> printa = decora(print)

>>> printa(1, 2, 3, sep='\t')


1 2 3

Evidentemente, queremos usar los decoradores para algo más que para obtener duplicados de otras
funciones. De esto se encarga el código que podamos insertar en las funciones decora y, sobre todo,
envoltorio.
Por ejemplo, podemos modificar decora para que diga algo bonito antes de ejecutar la función decorada:

>>> def decora(función):


... def envoltorio(*args, **dicc):
... print('Algo bonito:\t', end='')
... función(*args, **dicc)
... return envoltorio
...

>>> printa = decora(print)

>>> printa(1, 2, 3, sep='\t')


Algo bonito: 1 2 3

La decoración suele aplicarse en el momento de la definición de la función. Existe una colección


de decoradores estándar que permiten realizar distintos cometidos. Dos de los más importantes son
staticmethod y classmethod, que permiten desvincular los métodos de una clase del objeto desde el que
se invocan, y cuya utilidad real, de existir, se limita a cuestiones estilísticas.
Para simplificar la aplicación del decorador, suele usarse una expresión decoradora, cuya forma es el
nombre del decorador, precedido de arroba (@) y colocado justo antes de la definición de la función
decorada.
Así, la construcción:

>>> def funcion():


... bloque
...

>>> funcion = decora(funcion)

puede sustituirse por la más compacta (y habitual):

>>> @decora
... def funcion():
... bloque

Memorización de resultados.

Una aplicación más útil de la decoración es la memorización de resultados (memoization). Este de-
corador es útil para almacenar el resultado de funciones costosas computacionalmente. Al invocar la
función, por ejemplo esPrimo(), el envoltorio busca primero si la tupla con los argumentos, que debe ser
hashable, está almacenada en el diccionario de evaluaciones previas de la función; si lo está, devuelve
el resultado sin necesidad de calcularlo; si no lo está, lo calcula y almacena argumentos y resultado en
el diccionario.

120
7.2. ELEMENTOS DE PROGRAMACIÓN FUNCIONAL EN PYTHON.

>>> def memoriza(funcion):


... memoria = {}
... def evalua(*args, **dicc):
... clave = hash(tuple(args) + tuple([Link]()))
... if clave in memoria:
... print('Estaba memorizado')
... return memoria[clave]
... else:
... print('No estaba memorizado')
... resultado = funcion(*args, **dicc)
... memoria[clave] = resultado
... return resultado
... return evalua
...

>>> @memoriza
... def suma(x, z): return x + z
...

>>> suma(1, 2)
No estaba memorizado
3

>>> suma(1, 2)
Estaba memorizado
3

>>> suma(4, 2)
No estaba memorizado
6

>>> suma(4, 2)
Estaba memorizado
6

Decoración con argumentos.

Si la decoración de funciones ya es un concepto enrevesado en sí mismo, si queremos añadir argumentos


a la función decoradora, aún lo es más, porque vamos a necesitar un nivel adicional de decoración.
El problema es el siguiente: la función decoradora sólo puede tomar un argumento, que es el nombre
de la función decorar. Así pues, ésta no puede tener más argumentos. La solución es crear una función
(decoraArgs) que tome los argumentos (argsDecora) y devuelva la auténtica decoradora, que es la que
se aplica para decorar la función:

>>> funcion = decoraArgs(argsDecora)(funcion)

La estructura necesaria tiene la forma:

>>> def decoraArgs(argsDecora):


... def decoraFunc(función):
... def envoltorio(*argsFunc, **diccFunc):
... return función(*argsFunc, **diccFunc)
... return envoltorio
... return decoraFunc

121
CAPÍTULO 7. PROGRAMACIÓN FUNCIONAL.

Un ejemplo sería la mejora de la memorización vista en el apartado anterior. En ella, el diccionario en


el que almacenamos los argumentos y resultados crece sin límite al realizar cálculos con argumentos no
vistos anteriormente. Podemos evitar este crecimiento desaforado imponiendo un tamaño máximo al
diccionario. Si deseamos introducir un nuevo resultado en él, y ya se ha alcanzado el tamaño máximo,
el elemento más antiguo del diccionario es eliminado.

1 def memoriza(maxElem=1000):
2 def memorizaMax(funcion):
3 edad = 0
4 memoria = {}
5 def evalua(*args, **dicc):
6 nonlocal edad, memoria
7 edad += 1
8 clave = hash(tuple(args) + tuple([Link]()))
9 if clave in memoria:
10 memoria[clave][1] = edad
11 return memoria[clave][0]
12 else:
13 if len(memoria) >= maxElem:
14 orden = sorted([Link](), key=lambda item: item[1][1])
15 del memoria[orden[0][0]] # borramos el más antiguo
16 memoria = dict(orden) # acelera futuras ordenaciones
17 resultado = funcion(*args, **dicc)
18 memoria[clave] = [resultado, edad]
19 return resultado
20 return evalua
21 return memorizaMax

Nótese que las variables edad y memoria han sido declaradas con el calificativo nonlocal en la función
evalua. Este calificativo indica a Python que los dos nombres hacen referencia a variables definidas en
un ámbito superior, en este caso, memorizaMax(). En el caso de no declararse de este modo, al realizarse
asignaciones dentro de la función se perdería la asociación original, creándose una nueva asociación
local, que taparía la externa y que no mantendría su valor entre llamadas.

122
Capítulo 8

Entrada/salida, cadenas con formato y


datos binarios.

8.1. Entrada/salida por terminal.

El modo más elemental de leer y escribir datos en Python es el uso del terminal, es decir: el teclado,
para introducir valores, y la pantalla, para visualizarlos. En ambos casos, la interacción se realiza
mediante cadenas de texto.
Ya se ha visto la escritura en pantalla utilizando el comando print() . En esencia, el comando permite
escribir la representación bonita de sus argumentos, proporcionada por el método __str__(), o en su
defecto la formal, proporcionada por __repr__().
La sintaxis completa del comando es:

print(*objetos, sep=' ', end='\n', file=[Link], flush=False)

Donde los argumentos por clave permiten gobernar el modo de escritura de los objetos:

sep: Cadena de texto a utilizar para separar la representación de los objetos. Por defecto es un
espacio en blanco.

end: Cadena de texto para finalizar la escritura. Por defecto es una nueva línea.

file: Si se proporciona, su argumento debe ser un objeto para el que esté definido el método
write(), que será llamado para realizar la escritura. Habitualmente corresponderá con un
fichero de texto abierto en modo de escritura. Por defecto es [Link], es decir, la salida
estándar o pantalla del terminal.
También puede ser el resultado de abrir un fichero en modo escritura de texto o la salida
de error estándar ([Link]). El primer caso se comentará al ver la escritura en fichero; el
segundo requiere importar, previamente, el módulo sys:

>>> import sys

>>> print('Esta es la salida estándar')


Esta es la salida estándar

>>> print('Esta es la salida de error estándar', file=[Link])


Esta es la salida de error estándar

123
CAPÍTULO 8. ENTRADA/SALIDA, CADENAS CON FORMATO Y DATOS BINARIOS.

flush: Si no se indica, o es igual a False, la salida será buffered o no, en función del modo de escritura
del argumento de file. Si es igual a True se fuerza la escritura tan pronto como los datos
estén disponibles.

Por su parte, la entrada de teclado, comando input(), tiene la sintaxis:

input(mensaje=None)

input() escribe el mensaje en pantalla, o nada, si no se indica el argumento, y espera a que el usuario
escriba en el teclado una cadena de texto rematada con nueva línea. El comando devuelve el contenido
de la cadena, sin ningún tipo de conversión:

>>> numero = input('Escribe un número: ')


Escribe un número: 23

>>> type(numero)
<class 'str'>

>>> numero
'23'

Si se desea obtener un resultado de un tipo distinto al de cadena de texto, hemos de realizar la


conversión explícitamente:

>>> numero = float(input('Escribe un número: '))


Escribe un número: 23

>>> type(numero)
<class 'float'>

>>> numero
23.0

8.2. Entrada/salida por fichero.

La gestión de la entrada/salida usando ficheros se basa en el empleo de una clase de objeto denominada
objeto tipo fichero . Esta clase engloba tanto los ficheros auténticos, entendidos como estructuras de
datos almacenadas en un disco duro o semejante, como otros objetos que implementan una serie de
métodos típicos de los mismos, como open(), close(), read() o write().
Ejemplos de objetos tipo fichero incluyen cosas como los enlaces de comunicación de internet o de
FTP, buzones de correo, tarjetas de sonido, etc. Todos ellos se gestionan de manera semejante, usando
los métodos mencionados más arriba. Es un ejemplo claro de lo que se denominó, en la Sección 6.3.2,
tipado patoso. Los mismos algoritmos que leen de un fichero y/o escriben en él, pueden ser usados
en un contexto diferente en tanto el objeto en cuestión presente el mismo interfaz que los ficheros de
verdad.
Los objetos tipo fichero suelen llamarse, también, objetos fichero a secas u objetos de flujo (streams).
Seguramente, la peor de las tres alternativas sea la de objetos fichero (a menudo simplificado a fichero),
ya que presupone una naturaleza física concreta, mientras los otros dos son más generales. Sin embargo,
lo habitual, y más sencillo, es utilizar este nombre para referirse a ellos. Así, cuando en este texto se
utilice el término fichero, se hará referencia, indistintamente, a la estructura de datos que almacena
un cierto contenido o al objeto que gestiona el acceso al mismo.

124
8.2. ENTRADA/SALIDA POR FICHERO.

En Python existen tres tipos de fichero: los ficheros de texto y dos tipos de fichero binario: con búfer
(buferados), y sin él (crudos). Cada tipo de fichero tiene una clase asociada distinta, con lo que intentar
una operación de un tipo usando un objeto de otro puede dar lugar a error.
El uso de búfers es interesante en situaciones en las que transmitir los bytes de grupos de poco tamaño
puede ser demasiado caro. Es el caso, por ejemplo, de las comunicaciones por red telemática, En estos
casos, lo ideal es utilizar una memoria intermedia, o búfer, de manera que la operación sólo se lleva a
cabo cuando ésta está llena. En otras ocasiones es necesario transmitir los datos tan pronto como se
dispone de ellos, por lo que no se usará búfer.
Los ficheros de texto siempre usan búfer, pero éste varía en función de cómo se abren y de la naturaleza
del dispositivo utilizado para la lectura y/o escritura. Por ejemplo, si se va a utilizar el terminal, lo
ideal es que los datos se gestionen de línea en línea y sin demora; sin embargo, si la operación se
efectúa a través de una red telemática, será preferible canalizar la entrada/salida a través de un búfer
que agrupe un número elevado de caracteres antes de efectuar la comunicación.

8.2.1. Comando open().

Un fichero es accesible al abrirlo usando el comando open() . Dependiendo de las opciones pasadas
al comando, el objeto devuelto corresponderá a uno de los tres tipos de fichero. El sistema elevará
una excepción si se intenta abrir un fichero con un modo no permitido o usando una combinación de
opciones no compatible.
La sintaxis de open() es:

open(file, mode='r', buffering=-1,


encoding=None, errors=None, newline=None, closefd=True, opener=None)

Donde los argumentos más importantes son los tres primeros, que tienen el significado siguiente:

file: En principio, nombre es el nombre del fichero que se quiere abrir. Alternativamente, Python
permite obtener un objeto fichero a partir del descriptor de un fichero ya abierto. El des-
criptor es un número entero que se puede obtener ejecutando el método [Link]().

mode: Modo en el que se abre el fichero. Las opciones básicas son:


Modo Significado
'r' Abre el fichero en modo de lectura. Es el modo por defecto.
'w' Abre el fichero en modo de escritura.
'a' Abre el fichero para añadir, escribiendo a partir del final del fichero.
'x' Abre el fichero para escritura exclusiva, elevando una excepción si el fichero
ya existe.
'+' Abre el fichero para actualización, permitiendo la lectura y la escritura.
La opción de actualización ('+') debe usarse en combinación con el modo de escritura,
formando 'w+', en cuyo caso el fichero se trunca a cero bytes en el momento de la apertura;
o combinado con el modo de lectura, formando 'r+', en cuyo caso el contenido del fichero
se mantiene al abrirlo.
Además, podemos especificar si el fichero es binario o de texto con las dos opciones
siguientes:
Modo Significado
't' Abre el fichero en modo de texto. Es el modo por defecto.
'b' Abre el fichero en modo binario.
Por ejemplo, para abrir un fichero en modo texto para lectura y escritura sin eliminar el
contenido previo, ejecutaríamos:

125
CAPÍTULO 8. ENTRADA/SALIDA, CADENAS CON FORMATO Y DATOS BINARIOS.

>>> f = open(fichero, 'r+t')

buffering: Estrategia de buferado. En ficheros abiertos en modo texto, un valor búfer = 1 selecciona
el búfer de una línea y un valor búfer > 1 implica el uso de un búfer del tamaño indicado
en bytes. En ficheros abiertos en modo binario, un valor búfer=0 desactiva el empleo del
búfer; cualquier otro valor búfer > 0 será usado como el tamaño en bytes del búfer.
El comportamiento por defecto depende del tipo de fichero:

Los ficheros binarios se abren usando un búfer de tamaño apropiado para la ar-
quitectura del ordenador. Típicamente, su valor es de 4096 o 8192 bytes, y está
almacenado en la constante io.DEFAULT_BUFFER_SIZE,.
Los ficheros de texto en modo interactivo, básicamente el terminal, pero también
cualquier dispositivo para el que el comando isatty() devuelve True, usan búfer de
línea. Esto es, el contenido se almacena en memoria intermedia hasta la aparición
de una cadena de fin de línea o el final del fichero, en cuyo momento se transfiere la
línea entera.
Los ficheros de texto para los que isatty() devuelve False se acceden del mismo
modo que los binarios, es decir, usando búfer. Hay que andar con cuidado con este
comportamiento, porque puede producir resultados no esperados. Por ejemplo, si
escribimos en un pipeline, la escritura no se realizará línea a línea, sino en bloques
de 4096 o 8192 caracteres.

8.2.2. Clases de tipo fichero.

Clase matriz [Link].

Las clases que gestionan los tres tipos de fichero son herederas de una misma clase matriz: [Link] ,
que implementa métodos comunes a los tres tipos y proporciona implementaciones abstractas para
operaciones que dependen del tipo de fichero.
La clase [Link] proporciona, entre otros, los métodos siguientes, que están disponibles para los tres
tipos de fichero:

close(): Vacía los búferes y cierra el fichero, liberando los recursos utilizados por el mismo.
Cualquier operación realizada sobre un fichero cerrado eleva la excepción ValueError,
salvo el propio cierre de un fichero ya cerrado, que, simplemente, no hace nada.
Todo fichero abierto debe ser cerrado una vez finalizada la interacción con el mismo.

closed(): Devuelve True si el fichero ya ha sido cerrado, y False si aún está abierto.

readable(): Devuelve True si es posible leer el contenido del fichero.

readline(size=-1):
Lee una línea del fichero. Si size ̸= -1, como máximo leerán size bytes. En ficheros
binarios, una línea es una lista de bytes terminada con el byte b'\n', pero que no lo
contiene. En los ficheros de texto, el carácter usado para terminar la línea depende del
sistema operativo, aunque es configurable al abrir el fichero. Suele ser igual a '\n'.

readlines(size=-1):
Lee el fichero completo, almacenando sus líneas en una lista. Si size ̸= -1, como má-
ximo leerán size bytes.

126
8.2. ENTRADA/SALIDA POR FICHERO.

writeable(): Devuelve True si está permitida la escritura en el fichero .

writelines(lines):
Escribe la lista de líneas lines en el fichero. Sólo se escribirán caracteres o bytes de fin
de línea si éstos están incluidos en las propias cadenas de la lista.

seekable(): Devuelve True si el fichero es de acceso aleatorio. Esto es, si es posible acceder a una
posición arbitraria del fichero para leer o escribir. En ficheros en los que sólo es posible
el acceso secuencial a los datos, como los pipelines, seekable() devuelve False.

seek(offset, whence=SEEK_SET):
Accede a una posición determinada del fichero. Se permite indicar la posición a partir
de la posición actual o del principio o final del fichero. Si el fichero no es seekable, eleva
la excepción OSError.
Aunque está definido para todos los tipos de fichero, sólo es efectiva en ficheros binarios
y que soporten la operación. En ellos, offset es el desplazamiento en bytes respecto a
la posición indicada por whence, que puede tener uno de los valores siguientes:

SEEK_SET: Usa el principio del fichero y sólo admite valores de offset no negativos.
SEEK_CUR: Usa la posición actual en el fichero y admite valores de offset tanto posi-
tivos como negativos, además de offset=0.
SEEK_END: Usa el final del fichero y sólo admite valores de offset no positivos.

Otros métodos, muy notoriamente read() y write(), son específicos del tipo de fichero, y están imple-
mentados en su clase particular.
[Link] también proporciona los mecanismos necesarios para recorrer las líneas del fichero como un
iterable, como en estructuras del tipo for linea in fichero:, y para construir gestores de contexto,
que se verán en la Sección 8.2.3 de este mismo capítulo.

Ficheros de texto; clase [Link].

Por defecto, en Python los archivos se abren en modo texto. También puede forzarse el empleo del
modo texto incluyendo el carácter 't' en la cadena de modo de apertura:

>>> fichero = open('[Link]', mode='+tw')

>>> fichero
<_io.TextIOWrapper name='[Link]' mode='+tw' encoding='UTF-8'>

El objeto retornado pertenece a la clase [Link], que es un tipo derivado de [Link] ,


a la que se le ha añadido la adaptación al idioma mediante el uso del locale. Entre otros, implementa
los siguientes métodos:

read(size=-1):
Lee un máximo de size caracteres del fichero y los devuelve en forma de cadena de texto.
Si size es negativo o None, lee hasta el final del fichero.

readline(size=-1):
Lee una línea de texto, hasta un máximo de size caracteres, del fichero y la devuelve en
forma de cadena de texto.

write(s): Escribe la cadena de texto s y devuelve el número de caracteres escritos.

127
CAPÍTULO 8. ENTRADA/SALIDA, CADENAS CON FORMATO Y DATOS BINARIOS.

seek(): El método seek(), heredado de la clase matriz [Link], está reescrito para los ficheros de
textos con el propósito de que no sirva para nada. Cualquier intento de posicionarse en un
punto del fichero distinto del actual provoca la elevación de la correspondiente excepción.

>>> ficOut = open('[Link]', mode='wt')

>>> [Link]('Prueba de escritura en fichero:\n')


32

>>> [Link]('Segunda línea\n')


14

>>> [Link]('Tercera y última línea\n')


23

>>> [Link]() # Siempre hay que cerrar los ficheros que se abren

>>> ficIn = open('[Link]') # Por defecto, mode='rt'

>>> [Link]()
'Prueba de escritura en fichero:\nSegunda línea\nTercera y última línea\n'

>>> [Link]() # Siempre hay que cerrar los ficheros que se abren

>>> ficIn = open('fichero .txt', mode='rt') # Volvemos a abrir el fichero

>>> print([Link]())
Prueba de escritura en fichero:
Segunda línea
Tercera y última línea

>>> [Link]() # Siempre hay que cerrar los ficheros que se abren

También podemos acceder al contenido del fichero como un iterable más:

>>> ficIn = open('[Link]', mode='rt')

>>> for linea in ficIn:


... print(linea, end='') # Las líneas ya tienen '\n'
...
Prueba de escritura en fichero:
Segunda línea
Tercera y última línea

>>> [Link]() # Siempre hay que cerrar los ficheros que se abren

Ficheros binarios: clases [Link] y [Link].

Para abrir un fichero en modo binario sin búfer se ha de especificar que el fichero es binario y que no
se usa búfer:

>>> fichero = open('python_apav.tex', mode='rb', buffering=0)

>>> fichero
<_io.FileIO name='python_apav.tex' mode='rb' closefd=True>

128
8.2. ENTRADA/SALIDA POR FICHERO.

El objeto retornado pertenece a la clase [Link] que, entre otros, implementa los siguientes métodos
particulares de la clase [Link] :

read(size=-1): Lee un máximo de size bytes del fichero y los devuelve como objeto del tipo bytes.
Si size es -1 (valor por defecto), read() lee hasta el final del fichero.

readinto(b): Lee un máximo de len(b) bytes del fichero y los almacena en el objeto b, que ha
de ser de tipo bytes. Es útil para leer repetidamente datos de la misma longitud
máxima, evitando tener que ubicar memoria para leer cada uno de ellos.

write(b): Escribe los datos contenidos en la variable de tipo bytes b en el fichero, devuelve el
número de bytes leídos.

Para abrir el fichero usando búfer se ha de especificar que el fichero es binario y se puede usar el valor
por defecto del parámetro buffering, o darle como valor un entero positivo. que no se usa búfer:

>>> ficOut = open('[Link]', mode='bw')

>>> ficOut
<_io.BufferedWriter name='[Link]'>

El objeto retornado pertenece a la clase [Link], que es un tipo derivado de [Link] .


Los métodos soportados son los mismos que para los ficheros binarios sin buffer, pero además dispone,
entre otros, de los siguientes métodos particulares:

peek(size=None): Método semejante al read(), pero que no avanza en la posición del fichero. De
algún modo, permite saber lo que viene a continuación, pero sin leerlo.

flush(): Fuerza la lectura o escritura de los valores almacenados en el búfer, vaciándolo.

Un problema de la lectura/escritura en modo binario es que no permite el uso de los tipos de datos
habituales:

>>> [Link](8)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
----> 1 [Link](8)

TypeError: a bytes-like object is required, not 'int'

Esto es así porque los objetos leídos o escritos por estos métodos han de ser del tipo bytes, o semejante,
que no figura entre los tipos usados habitualmente. Uno de los principales usos de este tipo de datos
es, justamente, permitir la lectura y escritura en ficheros binarios. Para poder almacenar y acceder
a datos de tipos normales es necesario utilizar el denominado empaquetado binario, explicado en la
sección siguiente.

8.2.3. Cierre de los ficheros y recogida de basura; gestores de contexto.

En principio, Python implementa recogida de basura (”garbage collection”, en inglés). Esta recogida
de basura es un proceso que se lanza periódicamente localizando porciones de memoria ubicadas para
almacenar valores que no van a poder ser accedidos nunca más. Por ejemplo, si creamos un objeto
dentro de una función, lo más habitual es que el objeto deje de ser accesible cuando salimos de la
función. El recolector de basura detecta estas situaciones y libera la memoria ubicada para crear el

129
CAPÍTULO 8. ENTRADA/SALIDA, CADENAS CON FORMATO Y DATOS BINARIOS.

objeto inaccesible. Este es el motivo por el que no es necesario, habitualmente, llamar al comando del
para liberar la memoria ocupada por un objeto1 .
El estándar de Python establece el recolector de basura como el mecanismo básico de liberación de
memoria. Lo cual no impide que haga otras cosas, no recogidas en el estándar, como cerrar los ficheros
abiertos que ya no son accesibles. Es lo que hace la implementación oficial, CPython, y la mayor parte
de implementaciones importantes (PyPy, Jython, etc.).
Sin embargo, existen varios motivos por los que sí es importante cerrar los ficheros que puedan quedar
abiertos:

En primer y destacado lugar, que no todas las implementaciones de Python tienen por qué
cerrar los fichero abiertos. La única tarea que el estándar encomienda al recolector de basura es
la liberación de la memoria ocupada por objetos inaccesibles, no el cierre de los ficheros abiertos.

Cuando se ordena la escritura de información en un fichero, ésta no se escribe inmediatamente,


sino que suele almacenarse en un búfer hasta que éste se llena o el fichero se cierra. Mientras
esto no ocurra, el acceso al fichero por otros procesos puede dar resultados arbitrarios, o ser
imposible, como ocurre en Windows.

Los sistemas operativos tienen límites duros del número de ficheros que un proceso, y el sistema
completo, puede mantener abiertos. Este límite suele oscilar entre 512 y 8192. En tareas con
un consumo elevado de ficheros, como por ejemplo el acceso a bases de datos organizadas en
directorios, este límite puede sobrepasarse con facilidad.
Pero, como también hay un límite duro para el sistema completo, múltiples usuarios dejando
unos cuantos ficheros abiertos cada uno, pueden llegar a bloquear el sistema.

Cuantas más cosas mantenga un programa activas, más memoria consume, y eso, además, suele
repercutir en su ralentización.

Por todos estos motivos (y uno añadido, que dice que ”es lo que todo buen programador debe hacer”),
los ficheros abiertos deberían ser cerrados siempre. Así pues, la estructura de un programa que abre
algún fichero debería ser:

fichero = open(...)

código

[Link]()

El problema aparece si en código se produce un salto que evite que se alcance la orden de cierre. Por
ejemplo, si hay una cláusula return o se eleva una excepción.
Los gestores de contexto , o estructura with, son un mecanismo que garantiza que todo fichero que se
abre va a ser cerrado, pase lo que pase. Su sintaxis es:

with recursos:
bloque

1
Aunque es útil disponer de un mecanismo que lo permita. Por ejemplo, en una función podemos crear una estructura
muy grande, pero que el programador sabe que no volverá a ser usada a partir de un cierto punto del programa. El
programador lo sabe, pero el recolector de basura no lo puede saber. Así que lo mejor es eliminar el objeto con el
comando del.
De hecho, el comando del no libera directamente la memoria asociada al objeto. Lo que hace es eliminar la vinculación
entre el nombre borrado y el objeto, y, por tanto, disminuir en uno el número de referencias del mismo. Sólo cuando este
número llegue a cero, el recolector de basura liberará realmente la memoria ocupada por el objeto.

130
8.2. ENTRADA/SALIDA POR FICHERO.

Donde recursos es una o más expresiones del tipo abre(recurso) as nombre, en la que abre() es la
función que accede al recurso (por ejemplo, open()) y nombre es el nombre con el que éste será accesible
dentro del bloque (por ejemplo, fpIn).
Puede especificarse más de un recurso en cada cláusula with. En este caso, los distintos recursos se
separan con coma (,).
Por ejemplo, podemos usar un gestor de contextos para arreglar las notas de los alumnos:

>>> # Estas son las notas de los alumnos antes de 'arreglarlas'

>>> formato = '<i256pd'

>>> tamaño = calcsize(formato)

>>> estructura = bytearray(tamaño * len(alumnos))

>>> open('[Link]', 'rb').readinto(estructura)


804

>>> for alumno in iter_unpack(formato, estructura):


... print(*alumno, sep='\t')
...
1 b'Blanca Agirrebarrenetxe' 9.5
2 b'Carles Balcells' 4.99
3 b'David Garcia' 7.0

>>> # Leemos las notas, las arreglamos y las escribimos en '[Link]'

>>> with open('[Link]', 'rb') as fpIn, open('[Link]', 'wb') as fpOut:


... while True:
... estructura = [Link](tamaño)
... if not estructura: break
...
... numero, nombre, nota = unpack(formato, estructura)
... nota -= 1
... estructura = pack(formato, numero, nombre, nota)
... [Link](estructura)
...

>>> # Leemos las notas arregladas

>>> with open('[Link]', 'rb') as fpIn: estructura = [Link]()

>>> posicion = 0

>>> while len(estructura) - posicion > 0:


... alumno = unpack_from(formato, estructura, posicion)
... print(*alumno, sep='\t')
... posicion += tamaño
...
1 b'Blanca Agirrebarrenetxe' 8.5
2 b'Carles Balcells' 3.99
3 b'David Garcia' 6.0

Algún comentario acerca del código anterior

Parece ser que el autor se ha empeñado en mostrar maneras distintas de hacer lo mismo...
Es muy probable que lo haya hecho a conciencia.

La apertura/lectura todo-en-uno de la línea en la que se lee por primera vez el fichero con

131
CAPÍTULO 8. ENTRADA/SALIDA, CADENAS CON FORMATO Y DATOS BINARIOS.

las notas de los alumnos, es un ejemplo de código que puede parecer muy pythónico (y
lo es), pero en el que se incumple el mandamiento de siempre cerrar los ficheros que se
abren:

>>> open('[Link]', 'rb').readinto(estructura)

Siempre que se llame simultáneamente a open() y a un método suyo, el descriptor del


fichero se pierde, y será imposible cerrarlo explícitamente después. Es un pecadillo ve-
nial que también se comete al ejecutar un script Python con la orden recomendada
exec(open(script).read()).

El retorcido bucle usado para arreglar las notas es otro ejemplo de la utilidad del operador
morsa, o expresión de asignación:

... while True:


... estructura = [Link](tamaño)
... if not estructura: break
... ...

A partir de la versión Python 3.8 está disponible el operador morsa, con lo que el código
anterior puede escribirse de manera mucho más compacta:

... while (estructura := [Link](tamaño)):


... ...

Los gestores de contexto no sólo se pueden usar para acceder a ficheros, sino que también sirven
para acceder a cualquier recurso del sistema que deba ser liberado de manera explícita. Es el caso,
por ejemplo, de los semáforos de la programación concurrente, de los sockets de las comunicaciones
telemáticas, o de los buzones de correo. Para ello, la clase usada como recurso debe tener definidos los
métodos __enter__(), que se ejecuta en la cláusula with y debe devolver un objeto de la clase, que se
puede asignar a un nombre nombre usando la cláusula as; y __exit__(), que se ejecutará en el momento
en el que se abandone la ejecución de bloque, sea cual sea la causa de ello.

8.3. Cadenas con formato.

En Python, el mecanismo estándar para escribir en pantalla es el comando print(), que escribe la
representación bonita de sus argumentos (str()), separados por un espacio en blanco y rematados por
un carácter de fin de línea.
Es habitual que se desee tener un mayor control de cómo se escriben los objetos en pantalla, o en
un fichero de texto, al proporcionado por el comando print(). Por ejemplo, en C existe la función
fprint(), que permite escribir cadenas de texto formadas por literales y la representación deseada de
un cierto número de variables.
En Python no existe un mecanismo semejante... porque existe otro mucho más potente y versátil:
las cadenas con formato. La idea consiste en construir una cadena de texto con la información que

132
8.3. CADENAS CON FORMATO.

deseamos representar del modo que lo queramos hacer, y, a continuación, usarla como mejor nos plazca:
como argumento de un comando print() o input(), como mensaje de una excepción, etc.
Existen tres mecanismos para generar las cadenas de texto: la arcaica, emulando el estilo la función
de C printf() con el operador %; la anticuada, con el método format(); y la moderna, con las cadenas-f
(f-strings). Es importante conocer las tres, porque, aunque sólo se recomienda usar la última de ellas,
es muy habitual encontrar código escrito con las otras dos, particularmente la primera.

8.3.1. Cadenas de texto al estilo de printf().

El primer mecanismo aparecido en Python para crear cadenas de texto con formato son las cadenas
al estilo de printf() usando la sobrecarga del operador %2 .
Esta forma de construcción de las cadenas con formato es la original de Python, y se mantuvo como la
única hasta el año 2008, en que se lanzó la versión 3.0 y, simultáneamente, se introdujeron múltiples
mejoras en la 2.6. Por este motivo es, probablemente, la forma más extendida de todas, aunque su uso
hoy en día está totalmente desaconsejado. No obstante, dado lo extendido de su uso previo, es muy
importante conocerla y comprender, aunque sea a grandes rasgos, su funcionamiento.
Además, muchos de las especificaciones de conversión son compartidas por los métodos más modernos
de construcción de cadenas con formato.
Las cadenas de texto al estilo de printf() consisten en utilizar una cadena de formato, semejante a
las usadas en C, seguida del operador % y los objetos a representar:

cadena-de-formato % objetos-a-representar

Donde cadena-de-formato incluye tanto texto literal como indicadores de sustitución, que son conjuntos
de caracteres que indican que se debe insertar la representación del objeto correspondiente de los
objetos-a-representar.

Los objetos a representar pueden ser un único objeto, o uno o más organizados en forma de tupla.
En cualquier caso, es obligatorio proporcionar tantos objetos como indicadores de sustitución haya en
la cadena de formato. El resultado de la operación es una cadena de texto en la que cada indicador
ha sido sustituido por la representación del objeto asociado, de manera semejante a la función de C
sprintf().

Los indicadores de sustitución tienen la forma:

%[conversión][anchura][.precisión][longitud]tipo

Donde, como de costumbre, los campos encerrados entre corchetes son opcionales. El significado de
los campos es:

conversión: Indicadores de conversión para los valores numéricos. Puede usarse más de uno, pero
la combinación no siempre tiene mucho sentido y/o efecto.
Cada conversión se indica con un carácter de los siguientes:

2
Este es un caso en el que la sobrecarga de un operador no respeta en absoluto el sentido original del mismo, ya que
la creación de cadenas de texto con formato guarda poca relación con el resto de la división entera. Sin embargo, su uso
se justifica por el símbolo usado ( %), elemento característico de las cadenas usadas en la función de C printf().

133
CAPÍTULO 8. ENTRADA/SALIDA, CADENAS CON FORMATO Y DATOS BINARIOS.

Conversor Significado
# Indica la modificación del comportamiento estándar y el uso de la forma
alternativa. Su efecto se indica en un esquema más abajo.
0 Rellenar con ceros a la izquierda los números más cortos que anchura.
- Justificar a la izquierda los números más cortos que anchura. Deja sin
efecto la conversión 0 si ambas se usan simultáneamente.
Un espacio en blanco como conversor inserta un espacio delante de los
números positivos. Facilita el alineado vertical de números positivos y
negativos.
+ Inserta un signo más (+) delante de los números positivos. Deja sin
efecto la conversión anterior (espacio en blanco).
Las alternativas marcadas con el conversor # dependen del tipo de dato y de su repre-
sentación:

En enteros en octal, # añade la cadena '0o' al inicio del número.


En enteros en hexadecimal, # añade la cadena '0x' o '0X' al inicio del número.
En números reales, # fuerza la aparición del punto decimal '.', incluso en las
situaciones en las que éste no se incluiría.

anchura: Anchura mínima en caracteres de la representación. En caso de que el valor requiera


menos caracteres que anchura, se rellena con espacios o ceros, en función del tipo de
datos y las opciones de conversión. En caso de que se requieran más, se utilizan tantos
como sea necesario.
Si anchura es igual a *, su valor es tomado de la tupla de objetos a representar, y el
objeto realmente representado es el siguiente a éste.

precisión: Número de decimales en la representación de valores numéricos.


Si precisión es igual a *, su valor es tomado de la tupla de objetos a representar, y el
objeto realmente representado es el siguiente a éste.

longitud: Sin efecto en Python, se mantiene por compatibilidad con printf() de C, donde puede
valer l, L o h para representar valores numéricos de longitud superior a lo normal.

tipo: Tipo del objeto a representar, indicado por uno de los caracteres siguientes:
Tipo Significado
dói Entero representado en decimal.
o Entero representado en octal.
x ó X Entero representado en hexadecimal, con minúsculas (x) o mayúsculas (X).
f ó F Número real en coma flotante.
e ó E Número real en notación científica, usando e o E como base del exponente.
g ó G Número real representado en coma flotante o notación científica dependiendo
del exponente y el número de dígitos significativos.
c Un único carácter, como cadena de texto de un único carácter o número
entero, en principio menor que 0x110000 = 1114112, que represente su código
UTF.
s Cadena de texto, obtenida invocando la función str() con el objeto a repre-
sentar como argumento.
a Cadena de texto, obtenida invocando la función ascii() con el objeto a re-
presentar como argumento.
r Cadena de texto, obtenida invocando la función repr() con el objeto a re-
presentar como argumento.
% Usado para introducir un tanto por ciento ( %), sin interpretar, en la cadena.

134
8.3. CADENAS CON FORMATO.

Por ejemplo:

>>> 'uno es %+03d, 2 es %s y 3 es %.2f\n' % (1, 'dos', 3.3591)


'uno es +01, 2 es dos y 3 es 3.36\n'

Esta forma de construir cadenas presenta numerosos inconvenientes, que no dejan de ser los mismos
inconvenientes que presenta su modelo del C. Los más importantes, sin duda, son la poca claridad de
las cadenas con el formato y la desconexión entre cada expresión tanto por ciento y la variable a la
que representa. Cuando el formato es sencillo, por ejemplo, printf("número=%d", numero), el sistema
es razonablemente claro. Pero, en cadenas más largas y/o complejas, la cosa se complica.
Por ejemplo, cuando el número de objetos es elevado y/o los indicadores de sustitución son complejos, la
cadena de formato puede llegar a ser un galimatías del estilo de "%#-12.4f\t%.4s\t% 7.2E\t%#+4x%#12.4G",
en la que añadir, eliminar o modificar alguno de sus elementos puede ser un verdadero quebradero
de cabeza. Por suerte, y como ya se ha comentado, en la actualidad existen alternativas mucho más
interesantes.

8.3.2. Cadenas de texto usando el método format().

En el año 2008 se introdujo en las versiones 3.0 y 2.6 de Python un mecanismo más potente para
generar las cadenas con formato: el método format() de la clase str.
El método format() sustituye cada referencia por la representación del objeto correspondiente. Una
referencia es una cadena de texto encerrada entre llaves ({}). Todo lo que no sea referencia es mantenido
inalterado en la cadena resultante. Para introducir literalmente una llave, se debe duplicar en la cadena
({{ o }}).
La sintaxis de las referencias sigue una gramática que se puede expresar como:

{[nombre][!conversion][:formato] }

nombre: Nombre usado para identificar el objeto entre los argumentos del método format().

conversion: Precedido por exclamación (!), indica la conversión aplicada al objeto para su represen-
tación.

formato: Precedido por dos puntos (:), especifica el formato de la representación.

A continuación se describe en mayor detalle cada uno de estos elementos:

Identificación del nombre del argumento.

El nombre del argumento puede indicarse o no, y, en caso de hacerse, puede ser un número o una cadena
de texto que pueda representar un identificador válido.
Si se omite el nombre, los argumentos se identifican posicionalmente, de manera análoga a las cadenas
tipo printf():

>>> 'primero={}, segundo={{{}}}, tercero="{}"'.format(1, 2, 3)


'primero=1, segundo={2}, tercero="3"'

Podemos hacer referencia explícita a un argumento posicional de format() usando su índice en la lista
de argumentos como nombre:

135
CAPÍTULO 8. ENTRADA/SALIDA, CADENAS CON FORMATO Y DATOS BINARIOS.

>>> 'primero={2}, segundo={{{0}}}, tercero="{1}"'.format(1, 2, 3)


'primero=3, segundo={1}, tercero="2"'

Si nombre es un identificador válido, la cadena hace referencia a un argumento del método format()
identificado por clave:

>>> '{docena} y {docenaYMedia}'.format(docenaYMedia=18, docena=12)


'12 y 18'

Fijémonos en que, dado que son argumentos por clave, la posición no influye en su colocación.
Es posible indicar un atributo del objeto, o su índice, si éste es compuesto:

>>> v = Vector((1,2,3))

>>> '{[Link]}'.format(vector=v)
'[1, 2, 3]'

>>> '{0[2]}'.format(v)
'3'

Incluso podemos combinarlos y utilizar repetidamente un mismo argumento:

>>> '{[Link][2]} {[Link][0]}'.format(vector=v)


'3 1'

Explicitación de la conversión del objeto.

Por defecto, format() usa la representación bonita del objeto, proporcionada por su método mági-
co __str__(). Puede forzarse el uso de la representación formal, proporcionada por __repr__(), con
la conversión '!r'; la bonita, con '!s'; o la del método __ascii__(), con '!a'. Todas ellas usan la
representación formal, en caso de no estar definido el método correspondiente:

>>> v = Vector((1,2,3))

>>> '{}'.format(v)
'[1, 2, 3]'

>>> '{!r}'.format(v)
'Vector([1, 2, 3])'

>>> '{!s}'.format(v)
'[1, 2, 3]'

>>> '{!a}'.format(v)
'Vector([1, 2, 3])'

Especificación del formato de la representación del objeto.

El comando format() permite una gran variedad de opciones para formatear las cadenas que repre-
sentan sus argumentos. Su sintaxis responde a la forma:

136
8.3. CADENAS CON FORMATO.

[[relleno]alineado][signo][#][0][anchura][agrupa][.precision][tipo]

La mayor parte de las opciones son equivalentes a las vistas para las cadenas al estilo printf(). Por
ejemplo, el signo, la anchura, la precision y el comportamiento de los modificadores 0 y #, son idénticos
a los de aquél.
También los tipos son mayormente idénticos a los de printf(), salvo por la adición de otros tres
adicionales:

b: Permite representar números enteros en formato binario. Con el modificador #, se añade la par-
tícula '0b' al inicio del número.

>>> '{binario:b} {binario:#b}'.format(binario=27)


'11011 0b11011'

n: Cambia temporalmente la descripción local de LC_CTYPE a LC_NUMERIC. El cambio afecta a la elección


del separador de miles y de la parte fraccionaria en la representación de los números.

%: Representa números en formato tanto por ciento:

>>> '{numero:8.2f}{tanto:8.2%}'.format(numero=0.75, tanto=0.75)


' 0.75 75.00%'

Las principales diferencias entre el formato usando el método format() y el de estilo printf() están en
las opciones relleno, alineado y agrupa:

alineado: Usa los caracteres '>', '<' y '^' para alinear a la derecha, a la izquierda y centrado:

>>> '|{cadena:<9s}|{cadena:^9s}|{cadena:>9s}|'.format(cadena='<^>')
'|<^> | <^> | <^>|'

Por defecto, alinea a la derecha ('>').

relleno: Opcionalmente, se puede especificar un carácter antes del de la alineado que se utilizará
para rellenar el campo hasta la longitud indicada:

>>> '|{cadena:·<9s}|{cadena::^9s}|{cadena:.>9s}|'.format(cadena='<^>')
'|<^>······|:::<^>:::|......<^>|'

>>> '|{punto:<<9s}|{punto:^^9s}|{punto:>>9s}|'.format(punto='.')
'|.<<<<<<<<|^^^^.^^^^|>>>>>>>>.|'

Por defecto, se usa el espacio en blanco.

agrupa: Si el modificador agrupa es igual a subrayado (_) permite agrupar los enteros binarios,
octales y hexadecimales en grupos de cuatro caracteres, y los numeros decimales en grupos
de tres (miles):

>>> '{0:_b} - {0:_o} - {0:_x} - {0:_d}'.format(4098905)


'11_1110_1000_1011_0101_1001 - 1750_5531 - 3e_8b59 - 4_098_905'

137
CAPÍTULO 8. ENTRADA/SALIDA, CADENAS CON FORMATO Y DATOS BINARIOS.

Si agrupa es igual a coma (,), los números decimales se agrupan de tres cifras en tres cifras
separadas por coma o el carácter propio del locale local:

>>> '{0:,f} - {0:,d}'.format(4098905)


'4,098,905.000000 - 4,098,905'

Por defecto, no se agrupan los dígitos; nótese, no obstante, cómo al agrupamiento aumenta
la legibilidad de las cifras largas.

Todos los parámetros excepto el nombre del argumento, pueden especificarse como argumentos del
método format(). Para ello, hemos de encerrar el nombre del argumento entre llaves y colocarlo en la
posición correspondiente:

>>> '|{punto:{izquierda}}|{punto:{centro}}|{punto:{derecha}}|'\
... .format(punto='.', izquierda='<<9s', centro='^^9s', derecha='>>9s')
'|.<<<<<<<<|^^^^.^^^^|>>>>>>>>.|'

Es, incluso, posible usarlos como argumentos posicionales, en cuyo caso la posición viene marcada por
la de la llave de apertura ({):

>>> '|{:{}}|{:{}}|{:{}}|'.format('·', '><9', ':', '^^9', '.', '<>9')


'|·>>>>>>>>|^^^^:^^^^|<<<<<<<<.|'

El único problema es que todo lo que se había ganado en claridad con respecto a las cadenas al estilo
de printf() se pierde, y el resultado es un galimatías comparable... aunque mucho más potente.

Método __format__(); la clase datetime.

Es posible definir una gramática especifica para formatear los elementos de una clase. Esto se realiza
implementando el método __format__() .
Las posibilidades de esta técnica son muchas, pero su complejidad la dejan al margen de los propósitos
de este curso. No obstante, hay un caso en el que este trabajo ya está realizado y es de sumo interés:
la clase datetime .
Esta clase permite el manejo de fechas y horas, y contiene toda una definición del formato de sus
elementos. Por ejemplo, define los siguientes tipos, entre otros (puede consultarse la colección completa
en códigos de formato de datetime ):

Directiva Significado
%a Día de la semana abreviado ('Lun', 'Mar', 'Mié', ...).
%A Día de la semana completo ('Lunes', 'Martes', 'Miércoles', ...).
%b Nombre del mes abreviado ('Ene', 'Feb', 'Mar', ...).
%B Nombre del mes completo ('Enero', 'Febrero', 'Marzo', ...).
%d Día del mes usando dos dígitos ('00', '01', ..., '31').
%y Dos últimos dígitos del año ('00', '01', ..., '99').
%Y Año completo ('2000', '2001', '2002', ...).
%S Segundos en dos dígitos ('00', '01', ..., '59').
%M Minutos en dos dígitos ('00', '01', ..., '59').
%H Hora en dos dígitos y con reloj de 24 horas ('00', '01', ..., '23').
... ...

Por ejemplo, para escribir la fecha y hora actuales utilizaríamos la expresión siguiente:

138
8.3. CADENAS CON FORMATO.

>>> from datetime import datetime as dt

>>> ahora = [Link]()

>>> f'{ahora:%A, %d de %B de %Y a las %H:%M:%S}'


'Wednesday, 18 de March de 2020 a las 01:00:24'

8.3.3. Cadenas-f (f-strings).

A finales de 2016 se introdujo un nuevo avance en la construcción de cadenas con formato: las cadenas-f
(f-strings). Una cadena-f es una cadena de texto semejante a las vistas en el apartado anterior, pero
en la que el nombre es sustituido por una expresion que se evalúa al construir la cadena, eliminándose
el uso del método format(). Para distinguirla de las cadenas de texto normales, la cadena-f viene
precedida por la letra f.
Las cadenas-f nacen de la constatación de que, a menudo, un mismo identificador es repetido hasta tres
veces: una como nombre de la cadena de formato, otra como clave del argumento del método format()
y una tercera como objeto a representar:

>>> numero = 12

>>> '{numero}'.format(numero=numero)
'12'

Usando cadenas-f, esto mismo se reduce a:

>>> f'{numero}'
'12'

Por lo que respecta a las opciones del formato, todo lo visto en el método format() es aplicable para
las cadenas-f.

Otras virguerías de las cadenas-f.

La expresion puede ser cualquier expresión válida en Python y puede usarse como sustituto de cualquier
de los campos vistos al estudiar el método format(). Incluso es posible anidarlas, consiguiéndose
expresiones cuyo descifrado es un auténtico reto:

>>> f'{2 ** 4: {2 ** 5}{"d" if not True else f".{3 ** 2}f"}}'


' 16.000000000'

>>> f'{2 ** 4: {2 ** 5}{"d" if True else f".{3 ** 2}f"}}'


' 16'

No obstante, lo realmente importante no es este uso psicodélico, sino la enorme simplificación y au-
mento de la legibilidad que aportan. De hecho, en la versión Python 3.8, se han introducido las
cadenas autodocumentadas . Estas cadenas resuelven la situación, común en mensajes de registro, de-
puración y error, en las que un mismo identificador se repite cuatro veces:

139
CAPÍTULO 8. ENTRADA/SALIDA, CADENAS CON FORMATO Y DATOS BINARIOS.

>>> numero = 12

>>> 'numero={numero}'.format(numero=numero)
'numero=12'

Con el nuevo formato, esta expresión se reduce a:

>>> f'{numero=}'
'numero=12'

Su sintaxis consiste en añadir un signo igual (=) después de la expresión a representar. El resultado
será la escritura de la expresión, seguida de un signo igual y la representación del resultado usando
las conversiones y el formato especificados de la manera habitual.
Es posible, incluso, evaluar expresiones complejas, y la cadena producida incluirá tanto la expresión
como su resultado:

>>> theta = 30

>>> print(f'{theta=} {cos(radians(theta))=:.3f}')


theta=30 cos(radians(theta))=0.866

En general, puede afirmarse con rotundidad que las cadenas-f son la única alternativa que debe
contemplarse al escribir código para versiones de Python que las soporten. Sin embargo, debido
a lo muy extendido del uso de las otras dos opciones, es imprescindible conocer éstas también.

8.4. Empaquetado binario de los datos.

Como se señala en la sección anterior, las operaciones de entrada/salida binaria exigen el uso de datos
de un tipo semejante al bytes. Sin embargo, como se comentó en la Sección [Link], el uso de este
tipo de datos suele resultar poco conveniente. La solución es el empaquetado de datos, que permite la
conversión de un conjunto arbitrario de valores en una estructura tipo bytes, y viceversa.
Las funciones asociadas al empaquetado están integradas en un módulo aparte del lenguaje, el struct,
que es necesario importar con la orden import struct, y son las siguientes:

pack(formato, valores):
Devuelve un objeto bytes construido a partir de los valores, que es un número variable
de objetos simples (incluidas las cadenas de texto) según las especificaciones indicadas por
formato.

pack_into(formato, búfer, posición, valores):


Semejante a pack(), pero los bytes se almacenan a partir de la posición del búfer. La utilidad
de esta función es que se pueden realizar múltiples conversiones utilizando el mismo búfer;
evitando, de este modo, tener que ubicar la memoria cada vez.

unpack(formato, búfer):
Devuelve una tupla de objetos a partir del contenido del objeto búfer y siguiendo las especi-
ficaciones indicadas por búfer.

140
8.4. EMPAQUETADO BINARIO DE LOS DATOS.

unpack_from(formato, búfer, posición):


Como unpack(), pero los datos se desempaquetan a partir de la posición del búfer.

iter_unpack(formato, búfer):
Como unpack(), pero devuelve un iterador de tuplas. En cada acceso al iterador, next()
desempaqueta los bytes necesarios de búfer, y devuelve los objetos indicados por formato en
forma de tupla.

calcsize(formato):
Calcula el tamaño en bytes necesario para almacenar los valores especificados por formato.

Como se puede deducir de la definición de las funciones de struct, la clave de las conversiones entre ob-
jetos tipo bytes y los tipos simples estándar de Python es el argumento formato. formato es una cadena
de texto en la que hay uno o más caracteres de tipo que indican el tipo del objeto a desempaquetar.
Algunos de los tipos reconocidos son:

formato Tipo C/C++ Tipo Python Tamaño


'c' char bytes de longitud 1 1 byte
'b' signed char int 1 byte
'B' unsigned char int 1 byte
'?' _Bool bool 1 byte
'h' short int 2 bytes
'H' unsigned short int 2 bytes
'i' int int 4 bytes
'I' unsigned int int 4 bytes
'q' long long int 8 bytes
'Q' unsigned long long int 8 bytes
'f' float float 4 bytes
'd' double float 8 bytes
's' char [] bytes variable
'p' char [] bytes < 256 bytes

El formato se construye concatenando los caracteres de los tipos almacenados en la estructura, en el


mismo orden en el que aparecen en ella. Si un mismo tipo aparece más de una vez, puede usarse el
número de veces seguido del carácter asociado al tipo.
Por ejemplo, para almacenar un entero, seguido de tres reales y de otro entero, usaríamos:

>>> estructura = pack('i3fi', 1, 2.0, 3.0, 4.0, 5)

>>> unpack('i3fi', estructura)


(1, 2.0, 3.0, 4.0, 5)

Hay que tener cuidado de que el formato corresponda con los datos pasados. Si el tipo no coincide al
crear la estructura, se eleva la excepción error indicando la discrepancia; si no coincide al desempa-
quetarla, el resultado es impredecible:

>>> unpack('i3Ii', estructura)


(1, 1073741824, 1077936128, 1082130432, 5)

Si los datos están organizados en un tipo compuesto, como una lista o una tupla, éste ha de ser
desempaquetado en la invocación :

141
CAPÍTULO 8. ENTRADA/SALIDA, CADENAS CON FORMATO Y DATOS BINARIOS.

>>> dosReales = [1.2, 3.4]

>>> tresEnteros = [5, 6, 7]

>>> estructura = pack('2f3i', *dosReales, *tresEnteros)

Si se desea almacenar el contenido de una estructura en un tipo compuesto, el usuario ha de realizarlo


explícitamente a partir de la posición de los datos:

>>> datos = unpack('2d3i', estructura)

>>> reales = datos[: 2]

>>> enteros = datos[2: ]

>>> print(reales, enteros)


(1.2, 3.4) (5, 6, 7)

También podemos acceder selectivamente a los datos almacenados en la estructura usando la función
unpack_from():

>>> reales = unpack_from('2d', estructura, 0)

>>> enteros = unpack_from('3i', estructura, calcsize('2d'))

>>> print(reales, enteros)


(1.2, 3.4) (5, 6, 7)

Es destacable, en este último ejemplo, el uso de la función calcsize() para colocar el puntero de lectura
en la posición correcta de la estructura.

8.4.1. Sexo, tamaño y alineado de los datos.

En principio, los datos se almacenan en una struct o se extraen de ella usando su formato nativo.
Esto es, su sexo, tamaño y alineado son los propios de la máquina en la que se ejecuta.

Sexo: El sexo de un ordenador (en inglés, endianness 3 ) es el orden en el que se almacenan los
bytes que forman un objeto. En algunas arquitecturas, como los procesadores de Motorola,
los bytes se escriben de más significativo a menos, de manera análoga a cómo se escriben
las cifras en los idiomas occidentales. Es el llamado orden big-endian. Sin embargo, en la
mayoría de arquitecturas, y muy especialmente los procesadores de Intel, el orden es el
inverso, empezando por los bytes menos significativos (little-endian).

Tamaño: El tamaño con que se almacena un cierto tipo depende de la máquina usada. Por ejemplo,
en C se define el tipo short como un entero de 2 bytes y el long como uno de no menos
de 4 bytes. El tamaño del tipo entero más usado, int, queda abierto a la implementación,
con la única restricción de que no puede ser mas corto que un short ni más largo que un
long.
3
El origen del término proviene de las sangrientas disputas descritas en Los Viajes de Gulliver entre los liliputienses
y los blefuscudianos, por culpa del lado por el que hay que empezar un huevo duro. La mayoría de los liliputienses
defendían que los huevos debían cascarse por el extremo pequeño, de ahí que se les denominara little-endians, mientras
que la mayoría de los blefuscudianos defendían lo contrario, de ahí su apelativo de big-endians. Como siempre en estos
casos, también había los llamados equidistantes, pero éstos recibían de los dos lados y nadie se acuerda de ellos.

142
8.4. EMPAQUETADO BINARIO DE LOS DATOS.

Alineado: Es habitual que los datos no se puedan almacenar en cualquier posición de memoria. por
ejemplo, la mayoría de arquitecturas exigen que un entero de cuatro bytes empiece en
una posición de memoria múltiplo de cuatro. En el caso de los enteros de ocho bytes, hay
arquitecturas que exigen que empiecen en un múltiplo de ocho, y otras que exigen que
empiecen en múltiplos de cuatro.

Cuando un fichero se va a leer en un ordenador con la misma arquitectura que la usada al escribirlo, no
hay problema en usar el formato propio de la misma (denominado formato nativo). El problema surge
cuando se quiere leer un fichero escrito en una arquitectura diferente. Ya que, si el sexo, tamaño y/o
alineado es diferente, el resultado que se leerá no será igual a lo escrito. Para evitar estos problemas,
lo mejor es escribir los datos en un formato portable.
El módulo struct permite indicar dos tipos (y medio) de formato portable:

formato Sexo Tamaño Alineado


'<' little-endian estándar ninguno
'>' big-endian estándar ninguno
'!' network estándar ninguno

El formato '!' es el usado en comunicaciones de red telemática, y es idéntico al formato '>', pero, según
la documentación, se ha añadido para facilitar la vida de los pobres desgraciados que no consiguen
recordar que las comunicaciones telemáticas son siempre big-endian.
Además de estos formatos portables, existen dos que no lo son:

formato Sexo Tamaño Alineado


'@' nativo nativo nativo
'=' nativo estándar ninguno

El formato '@' es el nativo de la máquina, y el usado por defecto. El '=' es algo raro, que no se sabe
para que sirve.
El indicador de sexo, tamaño y alineado se coloca al principio de cadena formato, y es muy importante
que se utilice el mismo indicador en lectura y en escritura:

>>> estructura = pack('@2i', 1, 2)

>>> unpack('@2i', estructura)


(1, 2)

>>> unpack('>2i', estructura)


(16777216, 33554432)

8.4.2. Empaquetado de cadenas de texto.

Hay dos mecanismos para almacenar cadenas de texto en una estructura de bytes: el formato 's' y el
'p'. La principal diferencia entre ambas es el modo como se almacenan los datos: en las cadenas 's'
se almacenan directamente, y la cadena puede tener cualquier longitud; en las cadenas 'p' se usa el
formato ’cadena de Pascal’, en el que la longitud de la cadena se almacena en un byte inicial (y, por
tanto, está limitada a len(cadena) < 256) y, a continuación, se escriben los caracteres.
En ambos, el entero colocado antes del carácter de formato se interpreta como la longitud de la
cadena, no como una cuenta de repetición, como es el caso de todos los demás tipos de datos. Si no

143
CAPÍTULO 8. ENTRADA/SALIDA, CADENAS CON FORMATO Y DATOS BINARIOS.

se proporciona la longitud, se toma igual a uno. Como la cadena 'p' dedica un byte al tamaño de la
cadena, el número de caracteres pasado en el formato debe ser incrementado en uno.
Si la cadena a representar es más larga que el espacio habilitado para ella, se trunca. Si es más larga,
en el caso de la cadena 's' se rellena con caracteres nulos, y en el caso de las cadenas 'p' se descartan
las posiciones de memoria no necesarias. De este modo, la longitud de la cadena desempaquetada
usando el formato 's' es siempre igual al especificado; mientras que, cuando se usa el formato 'p',
puede ser más pequeño:
Así, usando cadenas 's', tenemos:

>>> cadenas = pack('6s9s12s', b'muy larga', b'muy justa', b'muy corta')

>>> larga, justa, corta = unpack('6s9s12s', cadenas)

>>> print(larga, justa, corta, sep='\n')


b'muy la'
b'muy justa'
b'muy corta\x00\x00\x00'

Mientras que con cadenas 'p' el resultado es:

>>> cadenas = pack('6p9p12p', b'muy larga', b'muy justa', b'muy corta')

>>> larga, justa, corta = unpack('6p9p12p', cadenas)

>>> print(larga, justa, corta, sep='\n')


b'muy l'
b'muy just'
b'muy corta'

En general, puede decirse que es más conveniente el formato 'p', aunque hay que tener en cuenta la
limitación de la longitud de la cadena a almacenar y el hecho de que se ha de reservar un byte de más
para almacenar su longitud.

8.4.3. Conversión entre bytes y str.

Las cadenas usadas y devueltas por las funciones de struct no son cadenas de texto, sino objetos del
tipo bytes, lo cual es engorroso ya que, aunque son formalmente semejantes y tienen prácticamente
todos los métodos propios de las cadenas, no pueden ser usados en las (muchas) situaciones en las que
se requiere objetos del tipo str.
Existen, fundamentalmente, dos clases de objetos del tipo bytes: la bytes, propiamente dicha, que es
inmutable; y la bytearray, que es idéntica a bytes pero mutable. Hay que tener esto en cuenta porque
cuando queramos escribir en el objeto, por ejemplo, al leer el contenido de un fichero, deberemos usar
siempre bytearray.
Una diferencia fundamental entre las cadenas de texto y los bytes es que estos últimos sólo admiten
los caracteres definidos en el conjunto básico de ASCII, que se limita a los siete primeros bits. El
conjunto extendido, hasta completar los 8 bits de un byte, se reservan para caracteres específicos del
idioma, como las vocales acentuadas, la ’ç’ o la ’ñ’. Pero este conjunto extendido no es común para
todos los idiomas, así que hay que especificar la página concreta de idioma que se va a utilizar. Por
ejemplo, la mayor parte de idiomas en Europa Occidental usa el estándar ISO/IEC 8859-1, mientras
que los idiomas con escritura cirílica usan el ISO/IEC 8859-5.
En el caso de las cadenas de texto de la clase str se usa el estándar UTF-8, que cubre, prácticamente,
las necesidades de todos los idiomas del mundo, pero a costa de ocupar más de un byte. La conversión

144
8.4. EMPAQUETADO BINARIO DE LOS DATOS.

entre los dos tipos es bastante sencilla, aunque es necesario especificar la codificación que se usa en
concreto; que generalmente es ’utf-8’:

Para construir un objeto tipo bytes a partir de otro tipo str se puede usar el constructor de la
clase, con el argumento opcional encoding='utf-8':

>>> bytes('sí', encoding='utf-8')


b's\xc3\xad'

Es importante remarcar que, mientras 'sí' es una cadena de dos caracteres, su codificación utf-8
y el consiguiente objeto de la clase bytes son de tres bytes.
También se puede usar el método encode() de la clase str, indicando la codificación deseada en
la invocación:

>>> 'sí'.encode('utf-8')
b's\xc3\xad'

Hay discusiones en el pythoniverso acerca de cuál de la dos opciones es más pythónica.

Para construir una cadena de texto a partir de un objeto tipo bytes también hay las mismas (o
análogas) dos posibilidades.
Usar el constructor de la clase str, con el argumento opcional encoding='utf-8':

>>> str(b's\xc3\xad', encoding='utf-8')


'sí'

O usar el método decode() de la clase bytes, indicando la codificación:

>>> b's\xc3\xad'.decode('utf-8')
'sí'

8.4.4. Lectura y escritura de ficheros binarios.

Usando las estructuras empaquetadas, su uso en la lectura y escritura de ficheros binarios es trivial.
Por ejemplo, el código siguiente escribe cuatro elementos de una lista de alumnos, cada uno de ellos
formados por un número entero de identificación, una cadena de texto con el nombre y un número
real con la nota; y, a continuación, los lee del fichero; todo ello con un formato portable:

>>> alumnos = [[1, b'Blanca Agirrebarrenetse', 9.5],


... [2, b'Carles Balcells', 4.99],
... [3, b'David Garcia', 7.0],
... ]

>>> formato = '<i256pd'

>>> tamaño = calcsize(formato)

>>> estructura = bytearray(len(alumnos) * tamaño)

>>> posicion = 0

145
CAPÍTULO 8. ENTRADA/SALIDA, CADENAS CON FORMATO Y DATOS BINARIOS.

>>> for alumno in alumnos:


... pack_into(formato, estructura, posicion, *alumno)
... posicion += tamaño
...

>>> fpOut = open('[Link]', 'wb')

>>> [Link](estructura)
792

>>> [Link]()

>>> open('[Link]', 'rb').readinto(estructura)


792

>>> for alumno in iter_unpack(formato, estructura):


... print(*alumno, sep='\t')
...
1 b'Blanca Agirrebarrenetse' 9.5
2 b'Carles Balcells' 4.99
3 b'David Garcia' 7.0

En el ejemplo de más arriba se pueden observar distintos planteamientos a la hora de manejar los
datos binarios y los ficheros. Se ha hecho adrede para ilustrar las distintas posibilidades. No obstante,
en un programa de verdad suele ser más conveniente adherirse a una metodología y ser coherente con
ella.
Seguramente, éste sea uno de los casos en los que el treceavo principio del Zen de Python no tiene
mucho sentido:

There should be one–and preferably only one–obvious way to do it.

Claro que, como indica su corolario, tal vez sí lo tenga para un holandés:

Although that way may not be obvious at first unless you’re Dutch.

146
8.4. EMPAQUETADO BINARIO DE LOS DATOS.

Lectura, procesado y escritura de ficheros WAVE.


El formato WAVE.
El formato WAVE es uno de los más extendidos para el almacenamiento y transmisión de señales
de audio. En el fondo, se trata de un tipo particular de fichero RIFF (Resource Interchange File
Format), utilizado no sólo para señales de audio sino también para señales de otros tipos, como
las imágenes estáticas o en movimiento, o secuencias MIDI.

La base de los ficheros RIFF es el uso de cachos (chunks, en inglés). Cada cacho, o subcacho, está
encabezado por una cadena de cuatro caracteres ASCII, que indica el tipo del cacho, seguido
por un entero sin signo de cuatro bytes, que indica el tamaño en bytes de lo que queda de cacho
sin contar la cadena inicial y el propio tamaño. A continuación, y en función del tipo de cacho,
se colocan los datos que lo forman.

Todo fichero RIFF incluye un primer cacho que lo identifica como tal y que empieza por la cadena
'RIFF'. A continuación, después del tamaño del cacho y en otra cadena de cuatro caracteres,
se indica el tipo concreto de información que contiene el fichero. En el caso concreto de los
ficheros de audio WAVE, esta cadena es igual a 'WAVE', y el cacho debe contener dos subcachos:
el primero, de nombre 'fmt ', proporciona la información de cómo está codificada la señal. Por
ejemplo, si es PCM lineal, ADPCM, etc., o si es monofónica o estéreo. El segundo subcacho, de
nombre 'data', incluye las muestras de la señal.

Dispone de una descripción detallada del formato WAVE en la página


WAVE PCM soundfile format de Soundfile.

Audio estéreo.
La mayor parte de los animales, incluidos los del género homo sapiens sapiens sanos y completos,
están dotados de dos órganos que actúan como transductores acústico-sensoriales (es decir,
tienen dos oídos). Esta duplicidad orgánica permite al bicho, entre otras cosas, determinar
la dirección de origen del sonido. En el caso de la señal de música, además, la duplicidad
proporciona una sensación de amplitud espacial, de realismo y de confort acústico.

En un principio, los equipos de reproducción de audio no tenían en cuenta estos efectos y sólo
permitían almacenar y reproducir una única señal para los dos oídos. Es el llamado sonido mo-
nofónico o monoaural. Una alternativa al sonido monofónico es el estereofónico o, simplemente,
estéreo. En él, se usan dos señales independientes, destinadas a ser reproducidas a ambos lados
del oyente: los llamados canal izquierdo (L) y derecho (R).

Aunque los primeros experimentos con sonido estereofónico datan de finales del siglo XIX, los
primeros equipos y grabaciones de este tipo no se popularizaron hasta los años 1950 y 1960. En
aquel tiempo, la gestión de los dos canales era muy rudimentaria. Por ejemplo, los instrumentos
se repartían entre los dos canales, con unos sonando exclusivamente a la izquierda, y el resto
a la derecha. Es el caso de las primeras grabaciones en estéreo de los Beatles: las versiones en
alemán de los singles ”She loves you” y ”I want to hold your hand”. Así, en esta última (de la que
dispone de un fichero en Atenea con sus primeros treinta segundos, Komm, gib mir deine Hand ),
la mayor parte de los instrumentos suenan por el canal derecho, mientras que las voces y las
características palmas lo hacen por el izquierdo.

Un problema habitual en los primeros años del sonido estereofónico, y aún vigente hoy en día,
es que no todos los equipos son capaces de reproducir los dos canales por separado. La solución
comúnmente adoptada consiste en no almacenar cada canal por separado, sino en la forma
semisuma, (L + R)/2, y semidiferencia, (L − R)/2, y de tal modo que los equipos monofónicos

147
CAPÍTULO 8. ENTRADA/SALIDA, CADENAS CON FORMATO Y DATOS BINARIOS.

sólo accedan a la primera de ellas. De este modo, estos equipos pueden reproducir una señal
completa, formada por la suma de los dos canales, y los estereofónicos pueden reconstruir los
dos canales estéreo.

Por ejemplo, en la radio FM estéreo, la señal, de ancho de banda 15 kHz, se transmite del modo
siguiente:

En banda base, 0 Hz ≤ f ≤ 15 kHz, se transmite la suma de los dos canales, L + R. Esta


es la señal que son capaces de reproducir los equipos monofónicos.

La señal diferencia, L − R, se transmite modulada en amplitud con una frecuencia de


portadora de 38 kHz. Por tanto, ocupa la banda 23 kHz ≤ f ≤ 53 kHz, que sólo es accedida
por los equipos estéreo, y, en el caso de colarse en un reproductor monofónico, ocupa la
banda no audible.

También se emite una sinusoide de 19 kHz, denominada señal piloto, que se usa para
demodular síncronamente la señal diferencia.

Finalmente, la señal de audio estéreo puede acompañarse de otras señales de señalización


y servicio en frecuencias entre 55.35 kHz y 94 kHz.
En los discos fonográficos, la semisuma de las señales está grabada del mismo modo que se
haría en una grabación monofónica, es decir, en la profundidad del surco; mientras que la
semidiferencia se graba en el desplazamiento a izquierda y derecha de la aguja. El resultado es que
un reproductor mono, que sólo atiende a la profundidad del surco, reproduce casi correctamente
la señal monofónica, mientras que un reproductor estéreo es capaz de separar los dos canales. Es
posible que algo de la información de la semisuma se cuele en el reproductor mono, pero, como
su amplitud es muy pequeña, se manifestará como un ruido muy débil, apenas perceptible.
En general, todos estos sistemas se basan en garantizar que el reproductor mono recibe correc-
tamente la semisuma de canales y que, si algo de la semidiferencia se cuela en la reproducción,
sea en forma de un ruido inaudible.

Tarea: conversión de estéreo a mono y viceversa.


Escriba el fichero [Link] que incluirá las cuatro funciones de gestión de señales estereofónicas
y monofónicas siguientes:

estereo2mono(ficEste, ficMono, canal=2):


Lee el fichero WAVE ficEste, que debe contener una señal de audio estéreo, y escribe el
fichero ficMono, que debe incluir una señal monofónica.

El tipo de señal concreto que se almacena en el fichero ficMono depende del valor del
argumento canal:

canal=0: Se almacena el canal izquierdo, L.


canal=1: Se almacena el canal derecho, R.
canal=2: Se almacena la semisuma de los dos canales, (L + R)/2. Es la opción por defecto.
canal=3: Se almacena la señal semidiferencia, (L − R)/2.

mono2estereo(ficIzq, ficDer, ficEste):


Lee los ficheros ficIzq y ficDer, que contienen las señales monofónicas correspondientes a
los canales izquierdo y derecho, respectivamente, y construye con ellas una señal estéreo
que almacena en el fichero ficEste.

148
8.4. EMPAQUETADO BINARIO DE LOS DATOS.

codEstereo(ficEste, ficCod):
Lee el fichero ficEste, que contiene una señal estéreo codificada con PCM lineal de 16 bits,
y construye con ellas una señal codificada con 32 bits que permita su reproducción tanto
por sistemas monofónicos como por sistemas estéreo preparados para ello.

La señal estéreo tiene el formato binario siguiente, donde cada recuadro representa un byte
y cada muestra ocupa dos enteros de dos bytes cada uno, uno para el canal izquierdo y otro
para el derecho:

x[n] x[n + 1]
... R2 L1 L2 R1 R2 L1 L2 R1 R2 L1 ...
16 bits 16 bits 16 bits 16 bits

La señal codificada debe tener el formato siguiente, donde cada muestra ocupa un entero
de cuatro bytes, con los dos bytes más significativos usados para codificar la semisuma de
los dos canales y los dos bytes menos significativos para codificar la semidiferencia:

x[n] x[n + 1]
L1 + R1 L2 + R2 L1 − R1 L2 − R2 L1 + R1 L2 + R2
... ...
2 2 2 2 2 2
32 bits 32 bits

Como la semidiferencia ocupa los 16 bits menos significativos, cada muestra es práctica-
mente igual a la semisuma, con un ruido que es 216 veces más pequeño (lo que representa
unos 90 dB por debajo y es, por tanto, prácticamente inaudible). Así pues, la señal es re-
producible tal cual por un sistema monofónico.

Sin embargo, un sistema preparado para ello es capaz de leer este fichero monofónico y
extraer de él los dos canales de la señal estereofónica.

decEstereo(ficCod, ficEste):
Lee el fichero ficCod con una señal monofónica de 32 bits en la que los 16 bits más signifi-
cativos contienen la semisuma de los dos canales de una señal estéreo y los 16 bits menos
significativos la semidiferencia, y escribe el fichero ficEste con los dos canales por separado.

Algunas observaciones:

Recuerde incluir su nombre completo en la cadena docstring del fichero. Además, cada
una de las cuatro funciones deberá incluir una cadena docstring con un mensaje de ayuda
que explique su funcionamiento.

Es muy recomendable escribir, además, sendas funciones que empaqueten y desempaqueten


las cabeceras de los ficheros WAVE a partir de los datos contenidos en ellas.

No se puede importar o usar ningún módulo externo, aparte del struct.

Se deben evitar los bucles. Se valorará el uso, cuando sea necesario, de comprensiones.

Los ficheros se deben abrir y cerrar usando gestores de contexto.

Las funciones deberán comprobar que los ficheros de entrada tienen el formato correcto y,
en caso contrario, elevar la excepción correspondiente.

149
CAPÍTULO 8. ENTRADA/SALIDA, CADENAS CON FORMATO Y DATOS BINARIOS.

Los ficheros resultantes deben ser reproducibles correctamente usando cualquier reproduc-
tor estándar; por ejemplo, el Windows Media Player o similar. Es probable, muy probable,
que tenga que modificar los datos de las cabeceras de los ficheros para conseguirlo.

Puede usar la señal Komm, gib mir deine Hand para probar sus funciones, pero recuerde
que, aunque sea en alemán, se trata de los Beatles. Así que procure no destrozar innece-
sariamente la canción.

150
Capítulo 9

Expresiones regulares.

Análogamente a lo que ocurre con la escritura de cadenas de texto con formato, en las que Python
carece de algo semejante a la función printf() de C/C++, tampoco existe una función semejante a
scanf(). Sin embargo, e igual que pasaba en aquel caso, Python sí dispone de un mecanismo que es
mucho más potente y versátil, aunque también mucho más complicado: las expresiones regulares .
El concepto de expresión regular (a veces abreviadas como regex o regexp) surge en los años 50
del siglo XX en los trabajos de Stephen Cole Kleene para el diseño de un esquema que permitiera
formalizar el lenguaje natural. Su popularización se produce con la aparición de los sistemas UNIX,
en los que aparece, primero, en el comando sed y, a continuación, en su sucesor awk y en el lenguaje
de programación orientado a texto perl.
La idea de las expresiones regulares es más sencilla (mucho más sencilla) que su uso: una expresión
regular define un patrón, y su manejo consiste en comprobar si una cadena de texto lo verifica o no,
identificando las partes del patrón que se corresponden con las de la cadena.
La expresión regular más sencilla es el literal. Un literal es cualquier cadena de texto que no incluya
ninguno de los caracteres especiales, que son:

'.' El punto es el carácter comodín: coincide con cualquier otro carácter.

'^' En principio, sólo puede colocarse al inicio de una expresión regular, donde el circunflejo
implica el inicio de la cadena, aunque se verán otros usos del circunflejo más adelante.

'$' En principio, sólo puede colocarse al final de la expresión regular, donde el dólar implica el
final de la cadena.

'*' Hace que la expresión regular coincida con cero o más repeticiones de la expresión regular
inmediatamente precedente.

'+' Hace que la expresión regular coincida con una o más repeticiones de la expresión regular
inmediatamente precedente.

'?' Hace que la expresión regular coincida con la expresión regular inmediatamente precedente
como máximo una vez (o sea, o está o no está).
El interrogante tiene un significado adicional. En general, las expresiones regulares son
avaras; esto es, tienden a coincidir con el segmento más largo posible. Añadiendo un '?' a
los tres anteriores ('*?', '+?' o '??'), su comportamiento se vuelve minimalista, coincidiendo
con el segmento más corto que verifica la expresión.
Finalmente, también veremos otros usos del interrogante más adelante.

'{...}' Las llaves permiten especificar un número mínimo y máximo de repeticiones de la expresión
regular inmediatamente anterior.

151
CAPÍTULO 9. EXPRESIONES REGULARES.

'[...]' Los corchetes permiten especificar un carácter perteneciente a un conjunto de los mismos.

'(...)' Los paréntesis se usan para agrupar sub-expresiones dentro de una expresión regular. Más
adelante se verá que esta agrupación tiene muchas aplicaciones.

'|' La barra vertical permite escoger entre dos expresiones regulares alternativas.

'\' La barra invertida anula el significado especial que pueda tener el carácter siguiente.
Como Python también interpreta de manera especial la barra invertida, si la colocamos en
una cadena de texto, o bien ésta es cruda (r'\') o bien tendremos que colocar otra barra
invertida delante ('\\') para anular su significado especial para Python.
La cosa puede complicarse aún más si también va a intenrvenir el shell o cualquier otro pro-
grama que interprete de manera especial la barra invertida. Es posible tener que encadenar
ocho o más barras invertidas ('\\\\\\\\') para indicar las dos barras invertidas que, por
ejemplo, aparecen en direcciones web ('https:\\[Link]').
Usar cadenas de texto crudas es una manera de, al menos, mitigar este problema.

9.1. Funciones y métodos del módulo re.

Como se ha comentado, la expresión regular más sencilla es el literal, que es una cadena de texto sin
ninguno de los caracteres especiales mencionados más arriba.
Por ejemplo:

1 >>> expresion = r'esto es una expresión regular sencilla'

Fijémonos en que, aunque en este caso no tiene ningún efecto, se ha definido usando el formato
crudo (r'...'). Esta es una práctica habitual al manejar para evitar el problema, ya comentado, de
interpretar prematuramente las barras invertidas.
El manejo de las expresiones regulares corre a cargo de las funciones definidas en el módulo re, con lo
que deberemos importarlo para poderlas usar:

1 >>> import re

Este mòdulo define varios métodos. La relación completa, junto con todas sus opciones, se puede
encontrar en el Contenido del módulo re , pero los más importantes son:

compile(patrón):
Donde patrón es la cadena con la expresión regular. El resultado de invocar esta función es un
objeto la clase re._sre.SRE_Pattern, preparado para identificar el patrón en una cadena de texto,
:

>>> regex = [Link](expresion)

>>> regex
[Link](r'una expresión regular sencilla')

>>> type(regex)
<class '_sre.SRE_Pattern'>

152
9.1. FUNCIONES Y MÉTODOS DEL MÓDULO RE.

Es útil compilar una expresión regular cuando se usa repetidamente, pero no es necesario com-
pilarlas para poder usarlas; también se puede llamar directamente a las funciones del módulo re
usando la expresión regular como primero de sus argumentos.
search(cadena):
Busca la primera coincidencia del patrón en la cadena. Devuelve None si no consigue localizar
ninguna coincidencia, y un objeto de la clase re._sre.SRE_Match si sí encuentra alguna.
match(cadena):
Igual que search(), pero exigiendo que la coincidencia ocurra al principio de la cadena
finditer(cadena):
Igual que search(), pero localizando todas las coincidencias no solapadas de patrón en cadena,
que devuelve en la forma de un iterador de objetos re._sre.SRE_Pattern.
findall(cadena):
Devuelve una lista de cadenas de texto con todas las coincidencias no solapadas del patrón en la
cadena.

Hay que destacar que una cadena vacía puede verificar la expresión regular. Por ejemplo, si buscamos
cero o más ocurrencias de una cierta expresión, y ésta no aparece en la cadena, search(), y el resto de
métodos de búsqueda en re, encontrará que se cumplen, exactamente, cero ocurrencias.
Como ejemplos, vamos a localizar la expresión regular definida más arriba usando los distintos métodos
y con distintos resultados:

>>> import re

>>> expresion = r'una expresión regular sencilla'

>>> regex = [Link](expresion)

>>> [Link]('No sé si esto es una expresión regular sencilla')


<_sre.SRE_Match object; span=(9, 47), match='una expresión regular sencilla'>

>>> [Link]('No sé si esto es una expresión regular sencilla') # es None

>>> [Link]('esto es una expresión regular sencilla, pero no tanto')


<_sre.SRE_Match object; span=(0, 38), match='una expresión regular sencilla'>

>>> [Link]('No sé si esto es una expresión regular sencilla' * 2)


['una expresión regular sencilla', 'una expresión regular sencilla']

>>> [Link]('No sé si esto es una expresión regular sencilla' * 2)


<callable_iterator object at 0x7f994848e9b0>

>>> list(_)
[<_sre.SRE_Match object; span=(17, 47), match='una expresión regular sencilla'>,
<_sre.SRE_Match object; span=(64, 94), match='una expresión regular sencilla'>]

En general, y para tener mayor agilidad en los ejemplos, en lo siguiente las funciones del módulo re
se ejecutarán como funciones independientes, para lo que será necesario añadir un primer argumento
con el texto de la expresión regular.

>>> [Link](r'expresiones regulares', 'adoro las expresiones regulares, ¿tú no?')


<_sre.SRE_Match object; span=(10, 31), match='expresiones regulares'>

Los objetos de la clase re._sre.SRE_Pattern soportan una serie de métodos, entre los cuales podemos
destacar:

153
CAPÍTULO 9. EXPRESIONES REGULARES.

group(grupos):
Devuelve uno o más subgrupos de la coincidencia. Un subgrupo es cualquier segmento de la
expresión regular encerrado entre paréntesis. El subgrupo 0 es la coincidencia completa, que
es también el resultado si no se indica el argumento grupos. Los siguiente subgrupos están
numerados a partir de uno (¡atención, a partir de uno, no de cero, que es la coincidencia
completa!). Eleva la excepción IndexError si alguno de los grupos es negativo o excede el
número de subgrupos definido en la expresión regular.

>>> encontrado = [Link](r'(una) (expresión) (regular)',


'esto es una expresión regular sencilla')

>>> [Link](0)
'una expresión regular'

>>> [Link](1, 3)
('una', 'regular')

El método group() es idéntico al método mágico __getitem__(), que sobrecarga los corchetes
de acceso a elementos de un tipo compuesto. Así que también podemos hacer:

>>> encontrado[0]
'una expresión regular'

>>> encontrado[2]
'expresión'

Aunque no se permiten ni las lonchas ni el acceso a más de un elemento cada vez.

groups():
Devuelve una tupla con todos los subgrupos coincidentes:

>>> [Link]()
('una', 'expresión', 'regular')

start(grupo), end(grupo) y span(grupo):


Devuelve, respectivamente, la posición del primer carácter, el post-último o ambos, engloba-
dos en una tupla, de la coincidencia del subgrupo indicado por el argumento opcional grupo.
Si no se indica grupo o ésta vale cero, devuelve los valores de la coincidencia completa.

>>> print([Link](1), [Link](1), [Link](1))


8 11 (8, 11)

9.2. Algunas expresiones regulares sencillas.

Aparte del literal, la expresión regular más sencilla es el punto (.), que coincide con cualquier carácter:

>>> [Link](r'. es un carácter cualquiera', 'este A es un carácter cualquiera')


<_sre.SRE_Match object; span=(0, 32), match='este A es un carácter cualquiera'>

>>> [Link](r'. es un carácter cualquiera', 'este es un carácter cualquiera')

154
9.2. ALGUNAS EXPRESIONES REGULARES SENCILLAS.

>>> [Link](r'. es un carácter cualquiera', 'este ? es un carácter cualquiera')


<_sre.SRE_Match object; span=(0, 32), match='este ? es un carácter cualquiera'>

El circunflejo (^) al inicio de la expresión regular fuerza el inicio de la cadena, y el dólar ($) al final
fuerza el final de la misma:

>>> regex = [Link](r'^....$')

>>> [Link]('uno')

>>> [Link]('dos')

>>> [Link]('tres')
<_sre.SRE_Match object; span=(0, 4), match='tres'>

>>> [Link]('cuatro')

>>> [Link]('cinco')

>>> [Link]('seis')
<_sre.SRE_Match object; span=(0, 4), match='seis'>

Se puede indicar un carácter tomado de un conjunto de ellos encerrándolos entre corchetes ([...]):

>>> regex = [Link](r'[uU][nN][oO]')

>>> [Link]('UNO')
<_sre.SRE_Match object; span=(0, 3), match='UNO'>

>>> [Link]('uno')
<_sre.SRE_Match object; span=(0, 3), match='uno'>

>>> [Link]('uNo')
<_sre.SRE_Match object; span=(0, 3), match='uNo'>

>>> [Link]('UnO')
<_sre.SRE_Match object; span=(0, 3), match='UnO'>

También se puede indicar un rango de caracteres, con el primer elemento separado del último por
un guion (-). Rangos típicos son los dígitos ([0-9]), las letras en minúscula ([a-z]) o las letras en
mayúscula ([A-Z]). También pueden agruparse colocando tantos rangos, o caracteres sueltos, como
sea necesario en el conjunto. El propio carácter guion (-) puede incluirse en el conjunto colocándolo
en la primera posición, donde pierde su significado especial.
Por ejemplo, la expresión que representa los números hexadecimales de cuatro cifras:

>>> regex = [Link](r'[0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F]')

>>> [Link]('9A3e')
<_sre.SRE_Match object; span=(0, 4), match='9A3e'>

>>> [Link]('9A3H') # 'H' no es un carácter válido en hexadecimal

>>> [Link]('9A3E')
<_sre.SRE_Match object; span=(0, 4), match='9A3E'>

155
CAPÍTULO 9. EXPRESIONES REGULARES.

Los corchetes también son útiles para desposeer a los caracteres especiales de su carácter especial
(salvo la barra invertida ('\'), que es demasiado obstinada, y el circunflejo ('^'), que sólo lo pierde si
está precedido por barra invertida o en una posición distinta de la inicial):

>>> [Link]('^un [$.^] en medio', 'un $ en medio, que ya no es especial')


<_sre.SRE_Match object; span=(0, 13), match='un $ en medio'>

>>> [Link]('^un [$.^] en medio', 'un . en medio, que ya no es especial')


<_sre.SRE_Match object; span=(0, 13), match='un . en medio'>

>>> [Link]('^un [$.^] en medio', 'un ^ en medio, que ya no es especial')


<_sre.SRE_Match object; span=(0, 13), match='un ^ en medio'>

Un circunflejo al principio del conjunto de caracteres invierte su interpretación: en lugar de coincidir


con cualquier carácter de la lista, lo hace con cualquiera salvo los indicados en ella.

>>> [Link]('el dígito [^0-9] no coincide', 'el dígito A no coincide')


<_sre.SRE_Match object; span=(0, 23), match='el dígito A no coincide'>

>>> [Link]('el dígito [^0-9] no coincide', 'el dígito 0 no coincide')

>>> [Link]('el dígito [^0-9] no coincide', 'el dígito 1 no coincide')

>>> [Link]('el dígito [^0-9] no coincide', 'el dígito + no coincide')


<_sre.SRE_Match object; span=(0, 23), match='el dígito + no coincide'>

>>> [Link]('si es dígito "[^0-9]" no coincide', 'si es dígito "+" no coincide')


<_sre.SRE_Match object; span=(0, 28), match='si es dígito "+" no coincide'>

>>> [Link]('si es dígito "[^0-9]" no coincide', 'si es dígito "0" no coincide')

Algunos grupos habituales de caracteres tienen un nombre especial:


Grupo Significado
\d Un dígito, equivale a [0-9].
\D Cualquier carácter distinto de dígito, equivale a [^0-9].
\s Cualquier carácter considerado espacio (espacio, tabulador, etc.).
\S Cualquier carácter distinto de los considerados como espacio.
\w Cualquier carácter que puede formar para de un identificador válido de Python. Incluye
las letras, los dígitos y el subrayado. Equivale a [0-9a-zA-Z_].
\W Cualquier carácter que no puede formar parte de un identificador válido. Equivale a
[^0-9a-zA-Z_].
Estos grupos especiales pueden usarse en cualquier lugar en el que se pueda usar un carácter, incluyendo
la formación de conjuntos de caracteres:

>>> [Link](r'\d\D[\da-z]', '9aa: "dígito"-"no dígito"-"dígito o minúscula"')


<_sre.SRE_Match object; span=(0, 3), match='9aa'>

>>> [Link](r'\d\D[\da-z]', '9?a: "dígito"-"no dígito"-"dígito o minúscula"')


<_sre.SRE_Match object; span=(0, 3), match='9?a'>

Aunque no se sustituye por ningún carácter, también es destacable el grupo especial \b:

156
9.3. ALTERNATIVA Y AGRUPAMIENTO.

Grupo Significado
\b Coincide con el extremo de la palabra. Aunque no coincide con nada, exige que la
palabra empiece o acabe en la posición en que se encuentre, por ejemplo, porque es el
principio o final de línea, o porque hay un espacio en blanco.

9.3. Alternativa y agrupamiento.

Una expresión regular permite coincidencias alternativas separándolas mediante la barra vertical (|):

>>> [Link](r'uno|tres|cinco', 'uno coincide')


<_sre.SRE_Match object; span=(0, 3), match='uno'>

>>> [Link](r'uno|tres|cinco', 'dos no coincide')

>>> [Link](r'uno|tres|cinco', 'tres coincide')


<_sre.SRE_Match object; span=(0, 4), match='tres'>

Al analizar el texto, la opción escogida será siempre la primera que coincida. Esto puede ser pro-
blemático si unas opciones están contenidas en otras. Por ejemplo, si lo que buscamos es un número
de uno o dos dígitos, podríamos usar la expresión regular r'[0-9]|[1-9][0-9]', pero eso no dará el
resultado deseado, porque no reconocerá correctamente los números de dos cifras:

>>> [Link](r'[0-9]|[1-9][0-9]', '7')


<[Link] object; span=(0, 1), match='7'>

>>> [Link](r'[0-9]|[1-9][0-9]', '71')


<[Link] object; span=(0, 1), match='7'>

Para obtener el resultado deseado hemos de garantizar que las subexpresiones que puedan incluir otras
aparecen antes que éstas (y no siempre es fácil conseguirlo). Así pues, la expresión que coincide con
números de una o dos cifras es r'[1-9][0-9]|[0-9]':

>>> [Link](r'[1-9][0-9]|[0-9]', '7')


<[Link] object; span=(0, 1), match='7'>

>>> [Link](r'[1-9][0-9]|[0-9]', '71')


<[Link] object; span=(0, 2), match='71'>

En ocasiones, es necesario delimitar un subconjunto de la expresión regular. Por ejemplo, si queremos


colocar una alternativa en el interior de una expresión literal:

>>> [Link](r'(uno|tres) cinco', 'uno coincide')

>>> [Link](r'(uno|tres) cinco', 'uno cinco coincide')


<_sre.SRE_Match object; span=(0, 9), match='uno cinco'>

>>> [Link](r'(uno|tres) cinco', 'tres cinco coincide')


<_sre.SRE_Match object; span=(0, 10), match='tres cinco'>

El agrupamiento se utiliza en muchas otras situaciones, aparte de las expresiones alternativas. Por
ejemplo, siempre que queramos manejar como una unidad una subeprexión de la expresión.

157
CAPÍTULO 9. EXPRESIONES REGULARES.

9.4. Repetición de sub-expresiones.

Los comandos de repetición de sub-expresiones son: el interrogante, la suma, el asterisco y las llaves.
El interrogante implica que la sub-expresión puede aparecer o no, pero, como máximo una vez:

>>> [Link](r'uno o dos?', 'uno o dos')


<_sre.SRE_Match object; span=(0, 9), match='uno o dos'>

>>> [Link](r'uno o dos?', 'uno')

>>> [Link](r'uno o dos?', 'uno o do')


<_sre.SRE_Match object; span=(0, 8), match='uno o do'>

Pero fijémonos que el comando sólo afecta al último carácter ('s'). Podemos usar los paréntesis para
que el comando afecte a una expresión de mayor longitud:

>>> [Link](r'uno( o dos)?', 'uno')


<_sre.SRE_Match object; span=(0, 3), match='uno'>

>>> [Link](r'uno( o dos)?', 'uno o dos')


<_sre.SRE_Match object; span=(0, 9), match='uno o dos'>

El signo suma (+) implica una o más repeticiones de la expresión, pero no cero:

>>> [Link](r'uno( o dos)+', 'uno')

>>> [Link](r'uno( o dos)+', 'uno o dos')


<_sre.SRE_Match object; span=(0, 9), match='uno o dos'>

>>> [Link](r'uno( o dos)+', 'uno o dos o dos')


<_sre.SRE_Match object; span=(0, 15), match='uno o dos o dos'>

El asterisco (*) implica cero o más repeticiones de la expresión:

>>> [Link](r'uno( o dos)*', 'uno')


<_sre.SRE_Match object; span=(0, 3), match='uno'>

>>> [Link](r'uno( o dos)*', 'uno o dos')


<_sre.SRE_Match object; span=(0, 9), match='uno o dos'>

>>> [Link](r'uno( o dos)*', 'uno o dos o dos')


<_sre.SRE_Match object; span=(0, 15), match='uno o dos o dos'>

Puede indicarse el número mínimo y máximo de repeticiones con el comando {minimo,maximo}.

>>> [Link](r'uno( o dos){2,3}', 'uno o dos')

>>> [Link](r'uno( o dos){2,3}', 'uno o dos o dos')


<_sre.SRE_Match object; span=(0, 15), match='uno o dos o dos'>

>>> [Link](r'uno( o dos){2,3}', 'uno o dos o dos o dos o dos o dos')


<_sre.SRE_Match object; span=(0, 21), match='uno o dos o dos o dos'>

Hay distintas alternativas para expresar el número de repeticiones:

158
9.5. GRUPOS AVANZADOS.

{numero}: Un único número encerrado entre paréntesis exige ese número exacto de repeticiones. Es
lo mismo que {numero,numero}.

{minimo,}: Si se omite el máximo, este se toma igual a ∞.

{0,}: Cero o más repeticiones; equivale a '*'.


{1,}: Una o más repeticiones; equivale a '+'.

{,maximo}: Si se omite el mínimo, este se toma igual a 0.

{,1}: Una o ninguna vez; equivale a '?'.

Estos comandos son avaros, lo cual quiere decir que coinciden con la cadena más larga posible:

>>> [Link](r'algo largo*', 'algo largoooooooooooooooooooooooo')


<_sre.SRE_Match object; span=(0, 33), match='algo largoooooooooooooooooooooooo'>

Podemos modificar el comportamiento de todos los comandos de repetición añadiéndoles un carácter


de cierre de interrogación (?), formando las secuencias '??', '+?', '*?' y '{min,max}?'. Añadiendo ?,
el comando se vuelve desprendido, con lo que se conforma con el número mínimo de repeticiones.
Este número puede ser cero:

>>> [Link](r'algo largo*?', 'algo largoooooooooooooooooooooooo')


<_sre.SRE_Match object; span=(0, 9), match='algo larg'>

Pero, aunque '*?' siempre puede coincidir con la cadena vacía, que lógicamente es la más corta posible,
no siempre lo hará ya que lo que ha de coincidir es la expresión regular entera:

>>> [Link](r'algo largo*? largo', 'algo largoooooooooooooooooooooooo largo')


<_sre.SRE_Match object; span=(0, 39), match='algo largoooooooooooooooooooooooo largo'>

9.5. Grupos avanzados.

El elemento más potente de las expresiones regulares son sus grupos. Se recomienda consultar su
sintaxis completa en la documentación oficial. En lo que queda de sección sólo se explicarán las opciones
más importantes.

Referencia a sub-expresiones precedentes.

La primera utilidad de los grupos se ha visto ya: permitir aplicar los comandos de repetición a seg-
mentos de más de un carácter.
Una segunda utilidad básica es la de permitir hacer referencia a un grupo anterior. Cada grupo de
una expresión regular tiene un índice que se corresponde con el orden en el que aparece el símbolo
de apertura de paréntesis ((). Una vez localizado el grupo en la cadena, podemos hacer referencia al
mismo usando una barra invertida seguida de su índice.
Como ejemplo sencillo podemos localizar palabras de cuatro letras que se repiten en la cadena:

159
CAPÍTULO 9. EXPRESIONES REGULARES.

>>> [Link](r'(\w{4}).*\1', 'es esto una prueba de lo que esto es?')


<_sre.SRE_Match object; span=(3, 33), match='esto una prueba de lo que esto'>

9.5.1. Acceso a partes concretas de una coincidencia.

El método groups() de una coincidencia permite acceder a los distintos grupos definidos en la expresión
regular:

>>> match = [Link](r'(\b\w+\b)\s+(\b\w+\b)\s+(\b\w+\b)', 'Albert Garcia Torres')

>>> [Link]()
('Albert', 'Garcia', 'Torres')

En este ejemplo, podríamos acceder al nombre de pila como el primer grupo de la coincidencia (índice
0), al primer apellido como el segundo (índice 1), y al segundo apellido como el tercero (índice 2).

Grupos anidados.

Los grupos se pueden anidar, incluyendo unos dentro de otros, con lo que comprender el ejemplo se
vuelve una especie de reto:

>>> [Link](r'(I (II (III))) \1 \2 \3', 'I II III I II III II III III')
<_sre.SRE_Match object; span=(0, 28), match='I II III I II III II III III'>

Añadiendo alguna pista, la expresión anterior es algo más fácil de entender:

>>> [Link](r'(I (II (III))) \1 \2 \3', 'I II III I II III II III III')
<_sre.SRE_Match object; span=(0, 31), match='I II III I II III II III III'>

También ayuda usar el método groups(), que ya se presentó al principio del capítulo:

>>> _.groups()
('I II III', 'II III', 'III')

Grupos identificados por nombre.

Como alternativa al uso de la identificación por índice, podemos usar grupos identificados por nombre.
Un grupo identificado por nombre tiene la forma siguiente:

(?P<nombre>grupo_identificado_por_nombre)

Una expresión regular identificada por nombre tiene, fundamentalmente, dos utilidades:

Hacer referencia a una sub-expresión localizada anteriormente de una manera más inteligible
que usando índices numéricos:

160
9.5. GRUPOS AVANZADOS.

>>> [Link](r'(?P<uno>1 (?P<dos>2)) - (?P=uno) - (?P=dos)', '1 2 - 1 2 - 2')


<_sre.SRE_Match object; span=(0, 13), match='1 2 - 1 2 - 2'>

Hacer referencia a un grupo concreto de una coincidencia usando su nombre, como si fuera la
clave de un diccionario:

>>> match = [Link](r'(?P<uno>1 (?P<dos>2)) - \1 - \2', '1 2 - 1 2 - 2')

>>> match['uno']
'1 2'

>>> match['dos']
'2'

Las sub-expresiones identificadas por nombre permiten un método adicional, groupdict(), que devuelve
un diccionario con el resultado de sus coincidencias:

>>> match = [Link](r'(?P<uno>1 (?P<dos>2)) - \2', '1 2 - 2')

>>> [Link]()
{'uno': '1 2', 'dos': '2'}

Grupos de prebúsqueda y no capturados.

En general, el análisis de una cadena de texto en busca de una expresión regular compuesta es secuen-
cial: primero se localiza el primer elemento de la expresión; si se encuentra, se comprueba si el texto
siguiente coincide con el siguiente elemento de la expresión; y así sucesivamente hasta completar la
expresión regular. Durante este proceso, cualquier subexpresión encerrada entre paréntesis es asignada
a un grupo de coincidencia, que puede ser referenciado más adelante dentro de la propia expresión re-
gular o accedido desde fuera con alguno de los métodos previstos ([Link](), [Link](),
etc.
Hay ocasiones en los que no queremos que se produzca esta asignación, denominada captura. Por
ejemplo, cuando en una expresión compleja en la que se usan referencias a grupos previos, queremos
intercalar una subexpresión que requiere del uso de paréntesis. Al hacerlo, todos los números de los
grupos siguientes al intercalado se incrementan en uno, con lo que tendremos que corregirlos. En otras
ocasiones, simplemente, estaremos interesados en evitar almacenar grupos superfluos o redundantes.
Para ello se dispone de los grupos no capturados, indicados con la forma:

'(?:grupo_no_capturado)'

En un grupo no capturado el análisis se produce de manera idéntica a un grupo estándar. Es decir,


después de comprobar su coincidencia, se prosigue con el análisis del resto de la expresión regular y
del texto a analizar. La única diferencia es que el grupo no se almacena.
En otras ocasiones, podemos estar interesados en que no sólo no se almacene el grupo, sino también en
que no se avance en el análisis. Es el caso de que queramos comprobar si la cadena de texto contiene,
o no, una cierta subexpresión antes de proceder a su análisis. Para ello se dispone de los grupos de
prebúsqueda, que pueden ser afirmativos (el grupo comprobado aparece a continuación) o negativos
(el grupo no aparece).
Un grupo de prebúsqueda afirmativa tiene la forma siguiente:

161
CAPÍTULO 9. EXPRESIONES REGULARES.

'(?=grupo_de_prebúsqueda_afirmativa)'

No hay muchas situaciones en las que este tipo de grupo sea imprescindible. En general, existirán
otras soluciones, no siempre más complejas, para hacer lo mismo. Por el contrario, el caso negativo es
mucho más interesante:

'(?!grupo_de_prebúsqueda_negativa)'

En este caso, se comprueba si la subexpresión no se cumple, permitiendo proseguir con el análisis de


la cadena si ese es el caso. Por ejemplo, la siguiente expresión regular almacena en el grupo fichero
los nombres de fichero que tienen extensión, pero que no es .sh, .csh o .zsh:

>>> noSh = r'(?!.*[.](sh|csh|zsh))(?P<fichero>.*[.].*)'

>>> [Link](noSh, '[Link]')

>>> [Link](noSh, 'fichero_sin_extensión')

>>> [Link](noSh, '[Link]')


<[Link] object; span=(0, 11), match='[Link]'>

9.5.2. Emulación de scanf().

Como se indicó al inicio del capítulo, el principal interés de las expresiones regulares para la progra-
mación en Python es la de proporcionar un mecanismo que proporcione funcionalidades semejantes a
las de la función de C/C++ scanf().
Con las herramientas vistas hasta ahora, ya somos capaces de realizar esta misión. De hecho, como se
avanzó entonces, de una manera mucho más potente y versátil que scanf().
Por ejemplo, la función C siguiente permite extraer el número de identificación, el nombre completo
y la nota de un alumno, separados por tabulador:

scanf("%d\t%[^\t]\t%f", &numero, nombre, &nota);

El equivalente usando expresiones regulares en Python sería:

>>> reNumero = r'(?P<numero>\d*)'


>>> reNombre = r'(?P<nombre>\w*( \w*){1,2})'
>>> reNota = r'(?P<nota>\d*([.]\d*)?)'
>>> reAlumno = [Link](reNumero + '\t' + reNombre + '\t' + reNota)

>>> blanca = [Link]('123\tBlanca Agirrebarrenetxe\t9')


>>> carles = [Link]('24\tCarles Balcells Iglesias\t4.99')

>>> [Link]()
{'numero': '123', 'nombre': 'Blanca Agirrebarrenetxe', 'nota': '9'}

>>> [Link]()
{'numero': '24', 'nombre': 'Carles Balcells Iglesias', 'nota': '4.99'}

162
9.5. GRUPOS AVANZADOS.

De hecho, casi sin querer, la solución usando expresiones regulares es más selectiva, ya que sólo acepta
como nombre del alumno dos o tres cadenas formadas exclusivamente por caracteres válidos1 . La
expresión regular que sí hubiera reemplazado literalmente la usada en C hubiera sido:

>>> reNombre = r'(?P<nombre>[^\t ]*)'

1
Válidos como identificadores de Python, o sea que los dígitos y el subrayado también se aceptarían. Pero, igualmente,
el control que proporciona la expresión regular es superior al sufrido "\%[^\t ]" de C, que permite colar cualquier cosa.

163
CAPÍTULO 9. EXPRESIONES REGULARES.

Tratamiento de ficheros de notas.


Con el final de curso llega la ardua tarea de evaluar las tareas realizadas por los alumnos durante
el mismo. Para facilitar esta tarea, se dispone de la clase Alumno que proporciona los datos fun-
damentales de cada alumno: su número de identificación (numIden), su nombre completo (nombre)
y la lista de notas obtenidas a lo largo del curso (notas). La clase también proporciona métodos
para añadir una nota al expediente del alumno (__add__()), para obtener la representación ofi-
cial del mismo (__repr__()) y para obtener la representación bonita (__str__()).

La definición de la clase Alumno es:

[Link]
1 class Alumno:
2 """
3 Clase usada para el tratamiento de las notas de los alumnos. Cada uno
4 incluye los atributos siguientes:
5
6 numIden: Número de identificación. Es un número entero que, en caso
7 de no indicarse, toma el valor por defecto 'numIden=-1'.
8 nombre: Nombre completo del alumno.
9 notas: Lista de números reales con las distintas notas de cada alumno.
10 """
11
12 def __init__(self, nombre, numIden=-1, notas=[]):
13 [Link] = numIden
14 [Link] = nombre
15 [Link] = [nota for nota in notas]
16
17 def __add__(self, other):
18 """
19 Devuelve un nuevo objeto 'Alumno' con una lista de notas ampliada con
20 el valor pasado como argumento. De este modo, añadir una nota a un
21 Alumno se realiza con la orden 'alumno += nota'.
22 """
23 return Alumno([Link], [Link], [Link] + [other])
24
25 def media(self):
26 """
27 Devuelve la nota media del alumno.
28 """
29 return sum([Link]) / len([Link]) if [Link] else 0
30
31 def __repr__(self):
32 """
33 Devuelve la representación 'oficial' del alumno. A partir de copia
34 y pega de la cadena obtenida es posible crear un nuevo Alumno idéntico.
35 """
36 return f'Alumno("{[Link]}", {[Link]!r}, {[Link]!r})'
37
38 def __str__(self):
39 """
40 Devuelve la representación 'bonita' del alumno. Visualiza en tres
41 columnas separas por tabulador el número de identificación, el nombre
42 completo y la nota media del alumno con un decimal.
43 """
44 return f'{[Link]}\t{[Link]}\t{[Link]():.1f}'

A menudo, las notas de los alumnos se almacenan en ficheros de texto en los que los datos de
cada alumno ocupan una línea con los distintos valores separados por tabulador.

El ejemplo siguiente muestra un fichero típico con las notas de tres alumnos:

164
9.5. GRUPOS AVANZADOS.

[Link]
171 Blanca Agirrebarrenetse 10 9 9.5
23 Carles Balcells de Lara 5 5 4.5 5.2
68 David Garcia Fuster 7.75 5.25 8

Escriba la función leeAlumnos(ficAlum) que lea un fichero de texto con los datos de todos los
alumnos y devuelva un diccionario en el que la clave sea el nombre de cada alumno y el contenido
el objeto Alumno correspondiente.

La función deberá cumplir los requisitos siguientes:

Sólo debe realizar lo que se indica; es decir, debe leer el fichero de texto que se le pasa
como único argumento y devolver un diccionario con los datos de los alumnos.

El análisis de cada línea de texto se realizará usando expresiones regulares.

Deberá entregarse un fichero de nombre XYZ_alumno.py, donde XYZ son las iniciales del
autor.

• El fichero debe proporcionar la definición de la clase Alumno y de la función


leeAlumnos().
• El fichero debe incluir un comentario con el nombre completo del autor, la fecha y
una breve descripción de su contenido.
• No está de más insistir: el nombre del fichero debe ser XYZ_alumno.py, y deberá propor-
cionar las definiciones indicadas, bien ejecutándolo con las órdenes run XYZ_alumno.py
o exec(open('XYZ_alumno.py').read()), bien importándolo con import XYZ_alumno.
No se admitirán ficheros de texto, cuadernos de jupyter, ni cualquier otra cosa.

La función leeAlumnos() debe incluir, en su cadena de documentación, la prueba unitaria


siguiente según el formato de la biblioteca doctest, donde el fichero '[Link]' es el
fichero mostrado como ejemplo al principio de este enunciado:

>>> alumnos = leeAlumnos('[Link]')


>>> for alumno in alumnos:
... print(alumnos[alumno])
...
171 Blanca Agirrebarrenetse 9.5
23 Carles Balcells de Lara 4.9
68 David Garcia Fuster 7.0

Evidentemente, es responsabilidad del autor comprobar que la prueba unitaria se pasa


satisfactoriamente antes de la entrega de la tarea.

Para evitar que diferencias debidas a espacios en blanco y/o tabuladores den lugar a error,
se recomienda efectuar las pruebas unitarias con la opción doctest.NORMALIZE_WHITESPACE.
Por ejemplo, [Link](optionflags=doctest.NORMALIZE_WHITESPACE).

165
CAPÍTULO 9. EXPRESIONES REGULARES.

Análisis de expresiones horarias.


En casi todos los idiomas más habituales, cualquier hora puede reducirse al formato estándar
HH:MM, donde HH es un número de dos dígitos, que representa la hora y está comprendido
entre 00 y 23, y MM es otro número de dos dígitos, que representa el minuto y está comprendido
entre 00 y 59.

No obstante, en el lenguaje hablado, es raro usar este formato estándar. En el caso del caste-
llano, existe una gran variedad de formatos. La lista siguiente alguna de las posibilidades más
frecuentes, aunque existen bastantes más:

08:27 Es el formato estándar. Cuando la hora es menor que 10, es posible representarla
con dos dígitos (08:27), o sólo uno (8:27). Los minutos se representan siempre con
dos (8:05).

8h27m Las horas o minutos menores que 10 pueden representarse usando uno o dos dígitos.
Las horas en punto pueden indicarse sin minutos (8h).

8 en punto Las horas exactas suelen indicarse con la partícula ”en punto”. En ese caso, es
habitual omitir la letra h después de la cifra.
Otras alternativas semejantes son las ”8 y cuarto”, las ”8 y media” o las ”8 menos
cuarto”.
En todos estos casos, el reloj empleado será de 12 horas y empezando en 1 (de 1
a 12). El resultado será ambiguo, ya que no sabremos si una cierta hora es AM o
PM, pero así es cómo se suele hablar (la gente queda a "las 11 en punto" para ir
a una fiesta, no a las "las 23 en punto". El resultado se devolverá siempre en el
rango de 00:00 a 11:59.

... de la mañana
Las expresiones horarias entre las 4 y las 12 pueden ir seguidas de la partícula ”de
la mañana”.
Análogamente, las horas entre las 12 y las 3 pueden ir seguidas de ”del mediodía”,
las horas entre las 3 y las 8 pueden serlo de ”de la tarde”, entre 8 y 4 de ”de la
noche” y entre 1 y 6 de ”de la madrugada”.
En estos casos, el reloj empleado es siempre de 12 horas (nunca se dice ”las 18 de
la tarde”, sino ”las 6 de la tarde”). Además la hora no puede ser cero, sino que,
en ese caso, se usaría 12.

Tarea: normalización de las expresiones horarias de un texto.


Debe escribirse la función normalizaHoras(ficText, ficNorm) que lee el fichero de texto ficText,
lo analiza en busca de expresiones horarias y escribe el fichero ficNorm en el que éstas se expresan
según el formato normalizado, con las horas y los minutos indicados por dos dígitos y separados
por dos puntos (08:27).

Cada línea del fichero puede contener, o no, una o más expresiones horarias, pero éstas nunca
aparecerán partidas en más de una línea.

Las horas con expresión incorrecta, por ejemplo, ”17:5” (en la expresión normalizada deben
usarse dos dígitos para expresar los minutos) u ”11 de la tarde” (la tarde nunca llega hasta esa
hora), deben dejarse tal cual.

Para la evaluación de la tarea se usará un texto con unas cien expresiones horarias, que incluirán

166
9.5. GRUPOS AVANZADOS.

tanto expresiones correctas como incorrectas. Una parte de la nota dependerá de la precisión en
su normalización.

Se recomienda empezar normalizando textos que sólo contengan expresiones correctas del tipo
más sencillo; es decir, con la forma ”18h45m”. La consecución de este objetivo garantiza una
nota mínima de notable bajo (7). La extensión al resto de formatos indicados y la detección de
expresiones incorrectas serán necesarias para alcanzar la nota máxima (10).

La tabla siguiente muestra un ejemplo de texto antes y después de su normalización, incluyendo


tanto expresiones horarias correctas como incorrectas.

Ejemplo de normalización de las expresiones horarias de un texto.


La llegada del tren está prevista a las 18:30 La llegada del tren está prevista a las 18:30
Tenía su clase entre las 8h y las 10h30m Tenía su clase entre las 08:00 y las 10:30
Se acaba a las 4 y media de la tarde Se acaba a las 16:30
Empieza a trabajar a las 7h de la mañana Empieza a trabajar a las 07:00
Es lo mismo 5 menos cuarto que 4:45 Es lo mismo 04:45 que 04:45
Tenemos descanso hasta las 17h5m Tenemos descanso hasta las 17:05
Las campanadas son a las 12 de la noche Las campanadas son a las 00:00

Son exactamente las 17:5 Son exactamente las 17:5


Cuando llegó, ya eran las 11 de la tarde Cuando llegó, ya eran las 11 de la tarde
El examen es a las 17 de la tarde El examen es a las 17 de la tarde
Cenamos en las 7 puertas Cenamos en las 7 puertas
No llegará antes de las 1h78m No llegará antes de las 1h78m
Corrió la maratón en 32h31m, pero no ganó Corrió la maratón en 32h31m, pero no ganó
Quedamos a las 23 en punto Quedamos a las 23 en punto

167
CAPÍTULO 9. EXPRESIONES REGULARES.

168
Capítulo 10

Scripts, módulos, entornos virtuales y


programas.

A lo largo del curso se han ido definiendo clases y funciones que realizan distintas misiones. Por ejemplo,
se ha creado la clase Vector, que permite el manejo de vectores sencillos, o la función descompon(), que
descompone un número entero en sus factores primos. A menudo, estas definiciones son tan útiles que
resulta conveniente almacenarlas en un fichero para no tener que escribirlas de nuevo cada vez que las
queremos usar.
En Python tenemos dos mecanismos para ejecutar las órdenes contenidas en estos ficheros:

Ejecución: Consiste en ejecutar cada línea del fichero como si se hubiera escrito en el punto de la
llamada. A los ficheros que se ejecutan de este modo se les suele denominar scripts.

Importación: Equivalente a ejecutar el contenido del fichero, pero, en principio, las definiciones en
él contenidas no se incorporan directamente al espacio de nombres del programa sino
en un módulo específico.

10.1. Ejecución de scripts.

La manera más sencilla para ejecutar un bloque de código en Python es usando la función exec().
El código a ejecutar debe pasársele como primer (y habitualmente único) argumento en la forma de
cadena de texto:

>>> exec('for i in range(5):\n\tprint(f"{i =: d} {i ** 0.5 =: .4f}")')


i = 0 i ** 0.5 = 0.0000
i = 1 i ** 0.5 = 1.0000
i = 2 i ** 0.5 = 1.4142
i = 3 i ** 0.5 = 1.7321
i = 4 i ** 0.5 = 2.0000

Fijémonos en que es necesario respetar los caracteres de fin de línea y el sangrado, en este caso
indicados con las secuencias de escape '\n ' y '\t '.
Si las órdenes a ejecutar están incluidas en un fichero, lo que se ha de hacer es abrirlo en modo de
lectura de texto, leer su contenido y pasárselo a la función:

>>> with open('algebra/[Link]', 'rt'):


... codigo = [Link]()
...

169
CAPÍTULO 10. SCRIPTS, MÓDULOS, ENTORNOS VIRTUALES Y PROGRAMAS.

>>> exec(codigo)
>>> Vector([1, 2, 3]) + Vector([4, 5, 6])
Vector([5, 7, 9])

Estas órdenes son usadas tan a menudo que suelen abreviarse en una sola orden, juntando la apertura
del fichero con su lectura y ejecución1 :

>>> exec(open('algebra/[Link]').read())
>>> Vector([1, 2, 3]) + Vector([4, 5, 6])
Vector([5, 7, 9])

10.1.1. Las funciones (obsoletas) execfile(), runfile().

Antes de la versión 3.3 de Python también existía la función execfile(), que permitía ejecutar el
código contenido en un fichero. Por motivos poco claros, pero que tienen que ver con el décimo tercer
principio del Zen de Python (Debería haber una, y preferiblemente solo una, manera obvia de hacerlo),
esta función se eliminó, recomendándose en su lugar la solución vista en el apartado anterior.
Durante una etapa de transición también estuvo disponible en algunos entornos integrados (IDE)
como Spyder o PyCharm, otra función que realizaba más o menos lo mismo: runfile(). El comando
no siempre existía, y, además, dejaba de estar disponible al ejecutar el mismo script desde fuera del
IDE. Por ejemplo, al hacerlo desde el sistema operativo.
En general, es mejor no llevar la contraria a los pythonisos: aunque esté disponible alguno de estos
comandos, y parezcan más concisos y convenientes, se recomienda usar el método propuesto por ellos:
exec(open(script).read()). Además, y en la medida de lo posible, será mejor usar el mecanismo de
importación en lugar de la ejecución.

10.1.2. El comando %run de ipython.

En el intérprete interactivo ipython existe el comando mágico %run que reemplaza a los obsoletos
execfile() y runfile(). Como en todos los comandos mágicos de ipython, el carácter inicial ( %) sirve
para invalidar el nombre del comando como identificador válido de Python. De este modo, es ipython el
único que puede manejar el comando. Así, si intentamos ejecutar un script que contenga este comando
desde fuera de ipython, se producirá irremediablemente un error.
Sin embargo, ipython también interpreta correctamente los nombres de los comandos mágicos si omiti-
mos el tanto por ciento y ese mismo nombre no está asignado a ningún objeto del espacio de nombres.
Por tantos, será más habitual que se usen órdenes como run, cd o ls que sus correspondientes %run,
%cd o %ls:

In [0]: %run algebra/[Link]


In [1]: run algebra/[Link]
In [2]: Vector([1, 2, 3]) + Vector([4, 5, 6])
Vector([5, 7, 9])

El comando %run incorpora muchas ventajas sobre las otras opciones de ejecución de scripts. Por ejem-
plo, el fichero se ejecuta como un programa independiente (standalone), con lo que es posible indicarle
1
Aunque se pierde la capacidad de cerrar el fichero, con lo que debemos fiarnos de que el recolector de basura se
encargue de ello, algo que el estándar no garantiza. Sin embargo, las implementaciones estándar de Python, como el
CPython, sí lo hacen. Por otro lado, no será habitual que tengamos que ejecutar tantos scripts como para que el hecho de
no cerrarlos sea problemático. De hecho, los pythonistas de [Link] recomiendan este método para ejecutar scripts.

170
10.1. EJECUCIÓN DE SCRIPTS.

argumentos en línea de comandos que serán pasados al script a través de [Link]. Se recomienda
ejecutar ?run para obtener una explicación de todas las opciones del comando.
Una de las principales aportaciones de %run es la de permitir la evaluación del tiempo de CPU
empleado en la ejecución de un script. Para ello sólo es necesario usar la opción -t:

In [0]: run -t algebra/[Link]

IPython CPU timings (estimated):


User : 0.00 s.
System : 0.00 s.
Wall time: 0.00 s.

En este ejemplo, el tiempo de ejecución es tan pequeño que ipython nos devuelve 0.00 s. Podemos afinar
más la estimación usando la opción -N numIte, que ejecutará el script numIte iteraciones e indicará tanto
el tiempo promedio empleado por una de ellas como el total.

In [0]: run -t -N 10000 algebra/[Link]

IPython CPU timings (estimated):


Total runs performed: 10000
Times : Total Per run
User : 3.67 s, 0.00 s.
System : 0.17 s, 0.00 s.
Wall time: 3.85 s.

Vemos que cada ejecución del comando sigue durando un tiempo insignificante, que podemos estimar
dividiendo por 10 000 el tiempo total empleado en ejecutarlo 10 000 veces: 3.85 s/10 000 = 385 µs.

10.1.3. Importación de módulos y paquetes.

En Python, toda ejecución se realiza en un módulo, que es una unidad independiente de ejecución
dotada de su propio espacio de nombres. Por defecto, al arrancar el intérprete, la ejecución se realiza
en el módulo __main__, cuyo nombre podemos comprobar consultando el nombre __name__:

>>> __name__
'__main__'

Podemos ver el espacio de nombres de un módulo invocando el comando dir(modulo). Si se ejecuta sin
argumentos, devuelve una lista con los nombres definidos en el espacio de nombres del módulo actual2 :

>>> dir()
['__annotations__', '__builtins__', '__doc__', '__loader__', '__name__', '__package__', ···
,→ '__spec__']

Es posible ejecutar el contenido de un script usando el comando import seguido del nombre del script en
formato módulo. Este formato consiste en el nombre completo del fichero, sustituyendo los separadores
de directorio por punto y eliminando la extensión .py:

2
En el ejemplo, dir() se ha ejecutado desde el intérprete por defecto de Python, el IDLE. Si se ejecuta el mismo
comando desde ipython3, aparecen muchos más nombres (más de 70), definidos por este entorno de desarrollo

171
CAPÍTULO 10. SCRIPTS, MÓDULOS, ENTORNOS VIRTUALES Y PROGRAMAS.

>>> import [Link]

Ahora bien, al importar el script, y contrariamente a lo que ocurre cuando se ejecuta usando exec()
o %run, los nombres definidos en el mismo no se incorporan al espacio de nombres del módulo desde
el que se realiza la importación. En vez de ello, lo que se incorpora es el nombre del módulo:

>>> dir()
['__annotations__', '__builtins__', '__doc__', '__loader__', '__name__', '__package__', ···
,→ '__spec__', 'ejercicios']

A su vez, el módulo ejercicios tiene su propio espacio de nombres que podemos consultar con el
comando dir(ejercicios):

>>> dir(ejercicios)
['__annotations__', '__builtins__', '__doc__', '__loader__', '__name__', '__package__', ···
,→ '__spec__', 'primos']

Vemos que dentro del espacio de nombres del módulo ejercicios aparece un segundo módulo, el
primos. Accedemos a él como si se tratara de un atributo del objeto ejercicios; es decir, uniendo
los dos nombres usando un punto como enlace ([Link]). De hecho, todo en Python es un
objeto de alguna clase; en este caso, de la clase module:

>>> type(ejercicios)
<class 'module'>

Finalmente, es en el módulo [Link] donde encontramos los nombres definidos en el fichero


ejercicios/[Link] (las funciones descompon(), esPrimo() y primos()), a los que accedemos también
como atributos:

>>> dir([Link])
['__annotations__', '__builtins__', '__doc__', '__loader__', '__name__', '__package__', ···
,→ '__spec__', 'descompon', 'esPrimo', 'primos']

>>> [Link]
<function descompon at 0x7fe211bb64c0>

[Link]. El comando import.

El comando import permite tres modos distintos de invocación:

import modulo
El script asociado al modulo se ejecuta. Los nombres definidos en él, que pueden ser otros
módulos, son incorporados como atributos del objeto modulo.

from modulo import objeto[, ...]


El script correspondiente se ejecuta (en su totalidad), pero sólo los objetos enumerados en la
invocación, que pueden ser más de uno separados por comas, son incorporados directamente
al espacio de nombres del módulo en el que se ejecuta el comando.

172
10.1. EJECUCIÓN DE SCRIPTS.

from modulo import *


Análogo al caso anterior, pero los objetos incorporados son todos aquéllos marcados para
ello. En el apartado siguiente se verá cuáles son estos objetos marcados para la importación.
Este modo de importación está, en principio, desaconsejado: al usar el asterisco, todos los
objetos definidos en el módulo se incorporan indiscriminadamente al espacio de nombres, con
lo que cabe la posibilidad de colisión con otros objetos definidos con anterioridad. Los métodos
de importación estándar nos protegen de estas colisiones, bien porque sólo importamos lo
que deseamos, bien porque lo hacemos en un espacio de nombres distinto (recuérdese que los
espacios de nombres son una honking idea, sea lo que sea que esto significa).
Sin embargo, hay muchas situaciones en las que import * puede ser una buena idea. Por
ejemplo, cuando queremos usar la funciones contenidas en módulo que conocemos bien (como
puede ser [Link] en una sesión interactiva. También es útil al construir módulos o paquetes.
En este caso, es incluso posible controlar qué se importará y qué no.

Además, en el caso de los dos primeros modos, puede renombrase el módulo u objeto importado usando
la cláusula as:

import modulo as alias

from modulo import objeto1 as alias1, objeto2 as alias2, ...

Respecto a la ejecución del script usando exec() o %run, la importación presenta una diferencia
importante: cada módulo se importa una sola vez. Al hacerlo, el intérprete incorpora el nombre del
módulo en una caché de nombres de manera que se evite su ejecución si él, o un elemento del mismo,
es importado más tarde.
Por ejemplo, si el fichero ejercicios/[Link] incluye la definición de las funciones esPrimo(),
descompon() y primos(), y ejecutamos el comando from [Link] import esPrimo, el intérprete
primero busca el módulo ejercicios en la caché de módulos importados. Si no lo encuentra, ejecuta el
script, añade los módulos ejercicios y [Link] a la caché, e incorpora la función esPrimo()
al espacio de nombres.
Si, más adelante, ejecutamos el comando from [Link] import descompon, primos, el intér-
prete encontrará el módulo [Link] en la caché. Por tanto, el script no se ejecutará de
nuevo, aunque las dos funciones sí se incorporarán al espacio de nombres.
Este modo de funcionamiento tiene un aspecto positivo, casi imprescindible, y otro indeseable:

Se garantiza que cada módulo se importa una y sólo una vez. Aparte de una ligera mejora en
las prestaciones, la principal ventaja es que se evita la inclusión recursiva de los módulos. Por
ejemplo, si modulo1 importa modulo2, y este último importa al primero, entramos en un bucle
que, si nadie lo impidiera, provocaría el agotamiento de los recursos del sistema.
Al garantizarse que cada módulo sólo se importa una vez, se elimina la posibilidad de bucles.

Por desgracia, este mecanismo implica que, si modificamos el código del módulo después de
haberlo importado, el modo estándar de importación no actualizará las definiciones incorporadas
en la primera importación. Esto es un problema especialmente durante el proceso de desarrollo
de los módulos.
Existen distintos modos de evitar este problema, pero todos ellos con sus propios inconvenientes:

• Usar exec() o %run durante el desarrollo de los módulos. Por desgracia, todas las ventajas
del mecanismo de importación se pierden. Así, los nombres no pueden ser aislados o renom-
brados fácilmente. Además, el riesgo de bucles permanece, con lo que tenemos que tener
mucho cuidado para evitar recursiones que lleven al colapso del sistema.

173
CAPÍTULO 10. SCRIPTS, MÓDULOS, ENTORNOS VIRTUALES Y PROGRAMAS.

• La biblioteca importlib incluye la función [Link]() que fuerza la relectura de un


módulo previamente importado. Sin embargo, salvo en los casos más triviales, su modo de
empleo resulta confuso y farragoso.
Así, si se ha importado un objeto con la sintaxis from modulo import objeto, conseguir que
se vuelva a leer la definición de objeto requiere importar explícitamente modulo, reimportar
modulo con [Link]() y volver a importar objeto como al principio.
Por ejemplo:

>>> from [Link] import esPrimo


>>> esPrimo(13) # La función está mal... dice que 13 no es primo
False

>>> # Editamos la función y corregimos el error

>>> import [Link] # Es necesario importar el módulo...


>>> [Link]([Link]) # ... para poder reimportarlo
<module '[Link]' from '/home/albino/APAV/ejercicios/[Link]'>

>>> esPrimo(13) # Pero la función sigue fallando


False

>>> from [Link] import esPrimo # Hay que volver a importarla


>>> esPrimo(13) # Ahora lo hace bien
True

• Si las dos soluciones anteriores no son satisfactorias, lo mejor que se puede hacer es aplicar
la solución del informático: salir del intérprete y volver a entrar... aunque es una solución
que puede ser cara cuando se está en medio de una sesión interactiva.
Sin embargo, cuando los scripts se ejecutan desde el sistema operativo, esta solución se
aplica automáticamente ya que cada ejecución arranca un intérprete nuevo y no es necesario
preocuparse de los cambios en los módulos, ya que siempre se usará su última versión.

10.2. Construcción de programas en Python.

A menudo, usaremos Python para escribir programas que deben ser ejecutados desde el sistema ope-
rativo, como cualquier otro programa, y no desde el interfaz de un intérprete. En este apartado se
verán los pasos necesarios para construir un programa con cara y ojos escrito en Python.

10.2.1. Ejecución de scripts desde el sistema operativo.

Los ficheros de código Python pueden ser ejecutados directamente desde el sistema operativo. El modo
más inmediato para hacer esto es pasándolo como argumento al intérprete:

usuario:~/APAV$ python3 ejercicios/[Link]

Este modo de operación tiene diversos problemas, que iremos resolviendo a lo largo de este apartado. El
primero de todos es que no hemos hecho nada. El fichero ejercicios/[Link] incluye la definición
de distintas funciones muy útiles, como esPrimo() o descompon(), pero, al ejecutar el comando anterior,
lo único que hacemos es definirlas y salir.
Podemos modificar el fichero para pedirle que sí haga algo; por ejemplo, descomponer en factores
primos el número 65 231. Para ello, añadimos la línea siguiente al final del fichero:

174
10.2. CONSTRUCCIÓN DE PROGRAMAS EN PYTHON.

ejercicios/[Link]
print(f'{descompon(65231)=}')

Ahora, ejecutando el programa desde la línea de comandos obtenemos el resultado deseado:

usuario:~/APAV$ python3 ejercicios/[Link]


descompon(65231)=(37, 41, 43)

Ahora bien, el fichero ejercicios/[Link] proporciona distintas funciones útiles, aparte de descompon(),
y podemos estar interesados en acceder a ellas, o hacerlo a descompon() con un objetivo distinto al de
mostrar la descomposición en números primos de 65231. En definitivas cuentas, es más probable que
estemos interesados en importar el fichero como un módulo que en realizar una ejecución concreta de
una de las funciones definidas en el mismo.
El problema es que, tal y como hemos modificado ejercicios/[Link], éste siempre mostrará la
descomposición de 65231, aunque no estemos interesados en ella:

>>> import [Link]


descompon(65231)=(37, 41, 43)

Hay dos maneras, compatibles entre sí, de prevenir este tipo de efectos:

Podemos determinar si el fichero está siendo ejecutado o importado observando el valor de la


cadena '__name__':
• Cuando ejecutamos el fichero, sea desde la línea de comandos, con un exec() o con el
comando ipython %run, su valor es igual a '__main__'.
• Cuando importamos el fichero como un módulo, __name__ es el nombre del módulo, y, por
tanto, __name__ != '__main__' (a no ser que seamos tan cafres de llamar '__main__' a nuestro
módulo).
Por tanto, podemos evitar la ejecución del código final cuando importamos el fichero como un
módulo protegiendo este código con una cláusula if __name__ == '__main__':.
ejercicios/[Link]
if __name__ == '__main__':
print(f'{descompon(65231)=}')

Suele ser más conveniente usar los módulos como módulos, destinados a ser importados, y los pro-
gramas como programas, destinados a ser ejecutados. En el caso del ejemplo, lo más adecuado se-
ría construir un programa, probablemente de nombre [Link] o, incluso, [Link],
que primero importaría [Link] y, a continuación, proporcionaría la descomposición
del número.
Sin embargo, el truquito de comprobar el valor de la variable __name__ puede ser interesante
también en módulos para, por ejemplo, ejecutar tests unitarios que comprueben su correcto
funcionamiento, o para realizar tareas de instalación o configuración.

Las dos opciones no son excluyentes. Por el contrario, está muy extendido el uso de una cláusula de
comprobación en los ficheros destinados exclusivamente a su ejecución como programas. El motivo de
esta práctica inútil e ineficiente puede estar en que la cláusula se ha convertido en una especie marca;
algo así como el main() de un programa en C/C++, que avisa al programador de que el programa
principal empieza aquí.
Optamos por la combinación de las dos técnicas y construimos el fichero [Link]:

175
CAPÍTULO 10. SCRIPTS, MÓDULOS, ENTORNOS VIRTUALES Y PROGRAMAS.

[Link]
1 from [Link] import descompon
2
3 if __name__ == '__main__':
4 print(f'{descompon(65231)=}')

[Link]. Permisos de ejecución y el hashbang.

Ahora ya disponemos de un script Python destinado a ser ejecutado como un programa. Sin embargo,
sigue siendo necesario llamar al intérprete para poderlo ejecutar:

usuario:~/APAV$ python3 [Link]


descompon(65231)=(37, 41, 43)

Sin embargo, si intentamos ejecutar directamente el fichero, tenemos un error:

usuario:~/APAV$ ./[Link]
-bash: ./[Link]: Permission denied

Para poder hacer de [Link] un auténtico programa ejecutable necesitamos dos cosas:

Proporcionar permisos de ejecución al fichero con el comando chmod. Puede consultar el modo
de empleo de este comando invocando man chmod o chmod --help, pero, simplificando, hacemos
que un fichero sea ejecutable ejecutando chmod +x fichero.

Informar al sistema operativo de qué intérprete hay que usar para ejecutar el fichero. Para lo
que se usará el denominado hashbang.

Modificamos los permisos del fichero, pero el resultado no es el deseado:

usuario:~/APAV$ chmod +x [Link]


usuario:~/APAV$ ./[Link]

Después de un buen rato, el programa sigue sin dar signos de vida. El problema es que el intérprete de
comandos (Bash, en este caso) intenta ejecutar el fichero como si fuera un script en su propio lenguaje,
y la primera línea del fichero contiene una orden, import, que Bash interpreta como un programa del
sistema operativo. Este programa sirve para salvar en un fichero de imagen el contenido de cualquier
servidor X en marcha en el sistema. Como no tenemos ningún servidor X en marcha, el programa
se queda bloqueado esperando a que haya uno (el VcXsrv no cuenta porque se está ejecutando en la
máquina Windows, no en la Ubuntu).

Uso de hashbang para usar el intérprete correcto.

Para que el shell interprete el fichero como un script de Python, y no como uno de Bash, podemos
usar un shebang apropiado. El shebang (también conocido como hashbang, sharpbang, hashpling y
unos cuantos nombres más) es una línea al inicio del fichero que empieza por los caracteres #! y que
sirve para indicarle al shell el programa que debe emplear para interpretar el script.
La sintaxis del hashbang es sencilla: la cadena #! debe aparecer al principio de la primera línea del
fichero que queremos hacer ejecutable. A continuación, y en la misma línea, debemos proporcionar el
path completo del intérprete que queremos que ejecute el script.

176
10.2. CONSTRUCCIÓN DE PROGRAMAS EN PYTHON.

En nuestro caso queremos que el fichero sea interpretado por el programa /usr/bin/python3. Así pues,
el hashbang que debemos colocar al principio del script es:

1 #! /usr/bin/python3

El Python y el hashbang en equipos MacOS.


En sistemas MacOS, el Python puede estar en cualquier sitio, menos tal vez en /usr/bin. Como
en cada instalación puede estar en un sitio distinto, debe localizarse el sitio concreto usando la
orden which python3 y colocar esa ruta en el hashbang.

Una alternativa, también válida en sistemas Ubuntu, es usar el comando /usr/bin/env para que
sea éste el que se encargue de localizar el Python:

1 #! /usr/bin/env python3

Sin embargo, en sistemas Ubuntu es mejor usar la ruta estándar, ya que el uso de env tiene
limitaciones en el paso de argumentos al intérprete de Python, además de representar una cierta
carga en el momento del arranque del programa.

Al introducir esta línea al principio del fichero, el script ya se comporta como un programa ejecutable:

usuario:~/APAV$ ./[Link]
descompon(65231)=(37, 41, 43)

10.2.2. Paso de argumentos en la línea de comandos.

El fichero [Link] ya se comporta como un auténtico programa ejecutable. Sin embargo, no es


muy útil: siempre descompone el mismo número, 65231.
Evidentemente, deseamos que nuestro programa tenga un comportamiento más general, permitiendo
descomponer cualquier número. El modo de hacer esto es pasar el número a descomponer a través
de la línea de comandos. En Python, de manera análoga a como se hace en C/C++, Bash y otros
lenguajes de programación, el acceso a los argumentos del programa se realiza a través de la variable
[Link], que es una lista de cadenas de texto en la que [Link][0] es el nombre del programa, y los
distintos [Link][1:] son los argumentos con los que se le invocó.
El paso y análisis de los argumentos de un programa en línea de comandos, el denominado interfaz
de usuario, es un problema más complejo que lo que pueda parecer; incluso para un programa tan
sencillo como [Link].
Tres son los principales aspectos a tener en cuenta a la hora de diseñar el interfaz de usuario de un
programa:

La estrategia de asignación de los argumentos en línea de comandos a las variables del programa.

El modo de informar al usuario de cuáles son los argumentos del programa y cómo se le pasan.

Tan importante o más que los dos anteriores: la correcta correspondencia entre la información
que se le pasa al usuario y lo que realmente hace el programa.

177
CAPÍTULO 10. SCRIPTS, MÓDULOS, ENTORNOS VIRTUALES Y PROGRAMAS.

Estos aspectos se manejarán con la biblioteca docopt, que permite una gestión muy potente del interfaz
de usuario. Por ahora, vamos a usar una solución más inmediata:

Si el primer argumento, [Link][1], puede interpretarse como un entero, el programa muestra


en pantalla su descomposición en factores primos.
Si no se cumple el punto anterior, el programa muestra el modo de empleo correcto.

[Link]
1 #! /usr/bin/python3
2
3 from [Link] import descompon
4
5 import sys
6
7 if __name__ == '__main__':
8 try:
9 numero = int([Link][1])
10 except:
11 print(f'Empleo: {[Link][0]} numero')
12 exit(-1)
13
14 print(f'{descompon(numero)=}')

10.2.3. El interfaz en línea de comandos según POSIX y GNU.

El paso de parámetros y argumentos a los programas es una cuestión de suma importancia. Existen,
básicamente, dos alternativas: pasarlos en tiempo de ejecución o por línea de comandos. La primera
suele requerir que el usuario introduzca texto o seleccione con el ratón o el teclado las opciones
deseadas. Por lo tanto, es difícil, por no decir imposible, de automatizar. La alternativa, el uso de
un interfaz en línea de comandos (CLI, por sus iniciales en inglés), es la preferida en sistemas del
estilo de UNIX, y es especialmente útil para lanzar programas con la mínima interacción posible (por
ejemplo, si se desea lanzar una tanda de experimentos ejecutando un mismo programa con parámetros
distintos).
Desde los inicios de los sistemas informáticos han aparecido infinidad de estrategias para el paso de
argumentos por línea de comandos. Siendo N el número de programadores, el número de criterios es,
seguramente, no inferior a 2N , ya que casi cada programador tiene su estilo preferido, que no le gusta
a casi nadie más, y casi nunca es el primero que desarrolló.
Para evitar esta dispersión, y muchas otras que existen en el ámbito informático3 , el IEEE lanzó en
los años 80 del siglo XX una iniciativa para unificar criterios, el llamado POSIX (acrónimo de Portable
Operating System Interface rematado en la X típica de sistemas operativos de la familia del UNIX,
como Linux, MacOS X, Xenix, AIX, etc.).

[Link]. Opciones y argumentos según POSIX.

Según POSIX, los argumentos de un programa se pueden dividir en dos tipos: opciones y operandos.
A su vez, las opciones se dividen entre las que tienen un argumento y las que no. Los aspectos más
relevantes de opciones y operandos son:

Opciones:
Una opción es un argumento formado por un guion ('-') y un único carácter alfanumérico.
Suele denominarse nombre de la opción al conjunto formado por ambos; así, suele decirse opción
menos hache a la cadena '-h'.
3
El estándar ocupa 6900 páginas, de las que sólo 8 se refieren estrictamente al interfaz en línea de comandos

178
10.2. CONSTRUCCIÓN DE PROGRAMAS EN PYTHON.

Hay dos tipos de opciones:


• Sin argumento de la opción, también conocidas como flags. Su valor es booleano: True,
si la opción está presente en la invocación; False, en caso contrario.
Por ejemplo, en muchos programas la opción '-v' activa un modo verboso que muestra
información más detallada de la ejecución del programa.
• Con argumento de la opción, en cuyo caso toma el valor de su argumento, que será una
sola palabra que puede estar unida o separada del nombre de la opción.
Por ejemplo: '-[Link]' o '-i [Link]'.
Si son necesarios más de un argumento, éstos se separarán con comas o con espacios.
En este último caso es muy probable que deba encerrarlos entre comillas para que el
shell los interprete como una sola palabra.
Las opciones sin argumentos pueden agruparse entre sí y con como máximo una opción con
argumento, que debe aparecer en último lugar. Por ejemplo, '-[Link]' es equivalente
a '-v -i [Link]'
Habitualmente, todo argumento iniciado por guion es una opción; aunque hay excepciones:
• Cuando el argumento es el valor que toma una opción. Por ejemplo, si se usa la opción
'-u umbral' para fijar un cierto umbral, en la expresión '-u -1', el '-1' es el valor que
toma el umbral, no una opción diferente.
• La cadena '--' aislada sirve para indicar el final de las opciones. Todos los argumentos
que se escriban después de ella serán interpretados como argumentos o comandos,
independientemente de si empiezan por guion o no.
• Un sólo guion aislado ('-') suele usarse para indicar que un fichero de entrada debe
ser sustituido por la entrada estándar (stdin). Si no hay ambigüedad, también puede
usarse para indicar que un fichero de salida debe ser sustituido por la salida estándar
(stdout).
En principio, las opciones pueden aparecer en cualquier orden, aunque cabe la posibilidad
de definir situaciones concretas en las que el orden sea relevante, y que deberá gestionar el
propio programa.
Las opciones deben anteceder al resto de argumentos. Es decir, no se pueden mezclar op-
ciones y argumentos.

Operandos:
Toda cadena de texto que no pueda ser interpretada como una opción o el argumento de una
opción es un operando. Son posicionales; es decir, su significado depende de la posición en que
se escriben.
En la invocación, los operandos deben ser sustituidos por el valor deseado.

[Link]. Extensiones y convenios de GNU.

A los criterios marcados por POSIX, GNU ha añadido extensiones y convenios que se han convertido
en un estándar de facto. Puede encontrarse información más completa en GNU Argument Syntax y en
GNU: Standards for Command Line Interfaces .
Estas extensiones no están contempladas por POSIX, pero no impiden el cumplimiento del estándar
en tanto en cuanto se mantenga la posibilidad de ejecutar el programa usando un CLI estrictamente
POSIX. Así es habitual permitir argumentos estrictamente POSIX junto con las extensiones de GNU.
Algunas de las extensiones más importantes aportadas por GNU son:

Nombres de opción largos: indicados por dos guiones seguidos del nombre de la opción, que será
de más de un carácter. Este tipo de opción no sólo está permitido sino que, por motivos de
claridad, es el más recomendado y se ha convertido en un estándar de facto.

179
CAPÍTULO 10. SCRIPTS, MÓDULOS, ENTORNOS VIRTUALES Y PROGRAMAS.

• El nombre largo deberá ser descriptivo del uso o comportamiento de la opción y, habitual-
mente, estará formado por una o más palabras separadas por guion ('--opción-larga').
• No es necesario escribir el nombre completo de la opción, sólo los caracteres necesarios para
identificarla unívocamente.
• Si la opción tiene un argumento, éste se escribirá separado de aquélla mediante espacios o
un signo igual ('=').
Por ejemplo, '--umbral=VALOR' es lo mismo que '--umbral VALOR'.
• Suele proporcionarse una alternativa corta, conforme al estándar POSIX, junto a cada
opción larga.
Por ejemplo, si existe la opción '--umbral=VALOR', será habitual que también exista la opción
'-u VALOR'.

Los argumentos posicionales se dividen en dos tipos: comandos y operandos propiamente dichos:

• Un operando propiamente dicho es uno cuyo valor indica el usuario en la invocación.


Por ejemplo, si un programa tiene como argumento 'fichero-entrada', el usuario deberá
escribir el nombre de un fichero en esa posición.
• Un comando es una cadena de texto que debe aparecer literalmente en la invocación.
Por ejemplo, los programas de manejo de paquetes apt, en Ubuntu, o brew, en MacOS, usan
multitud de comandos (update, install, remove, etc.) que indican las posibles operaciones a
realizar.
• Para distinguir comandos y operandos, los primeros se indican usando exclusivamente letras
minúsculas, dígitos y subrayados("esto_es_un_comando"); mientras que los operandos usa-
rán mayúsculas, dígitos y subrayados ("ESTO_ES_UN_OPERANDO"), o estarán encerrados entre
corchetes oblicuos (signos menor < y mayor >), en cuyo caso tienen formato libre ("<Esto
es otro operando>").
Los mismos criterios de nomenclatura se aplican a los argumentos de las opciones.

Salvo que se indique lo contrario, opciones y argumentos pueden aparecer en cualquier orden.
Es decir, no es necesario que las opciones aparezcan antes de los argumentos.

Las opciones -h, --help y --version suelen tener un significado especial: las dos primeras muestran
la sinopsis del programa, la última su versión.

[Link]. La sinopsis del programa.

Todo programa debe contar con una sinopsis que indique su modo de empleo. Aunque POSIX no lo
especifica, las opciones '-?' o '-h' suelen ser indicativa de que el usuario quiere ver esta sinopsis en
pantalla. En los programas que usan las extensiones de GNU, las opciones '-h' o '--help' cumplen
esta misión.
Los elementos más relevantes de la sinopsis son:

Si un programa tiene distintos modos de operación, con opciones diferentes, cada modo se escribe
en una línea separada.
A menudo, los programas tienen tres modos de operación: el modo normal, en el cual se ejecuta
el programa para que realice el cometido para el que se ha escrito; el modo de ayuda, que muestra
un mensaje en pantalla informando de cómo se ha de invocar; y el modo versión, que muestra
la versión del programa y finaliza la ejecución.

Los argumentos de valor variable pueden escribirse usando sólo mayúsculas, dígitos y subrayados
('ESTO_POR_EJEMPLO') o encerrados entre corchetes oblicuos ('<Esto otro>').

180
10.2. CONSTRUCCIÓN DE PROGRAMAS EN PYTHON.

Toda cadena compuesta exclusivamente de minúsculas, dígitos y subrayados se interpreta como


un comando, y debe escribirse tal cual en la invocación. Por ejemplo, en la orden 'sudo apt
install ...', la palabra 'install' es un comando que debe escribirse literalmente.

Los argumentos opcionales se escriben encerrados entre corchetes ('[argumento-opcional]'). Por


ejemplo, '[-v]' es un argumento opcional, que puede especificarse o no.

Los argumentos obligatorios pueden aparecer tal cual o encerrados entre paréntesis. Por ejemplo,
'(<argumento-obligatorio>)' o, simplemente, '<argumento-obligatorio>'.
El uso de los paréntesis sólo suele ser conveniente para agrupar argumentos alternativos o que
pueden repetirse más de una vez.

Los argumentos alternativos, es decir, mutuamente excluyentes, se escriben separados por barra
vertical ('|') y, a menudo, encerrados entre corchetes si son opcionales, o entre paréntesis si son
obligatorios.

Cuando un argumento puede ser repetido un número indefinido de veces, se indica con tres
puntos (elipsis o '...') a continuación del argumento correspondiente.
El operador elipsis es unario y sólo afecta al argumento o grupo de argumentos precedente. Para
que afecte a más de un argumento, todo el conjunto debe estar encerrado entre paréntesis o
corchetes.

[Link]. Ejemplo de sinopsis conforme al estándar POSIX con las ampliaciones de


GNU.

El recuadro siguiente muestra una sinopsis típica siguiendo las recomendaciones de POSIX con las
extensiones de GNU. Es un ejemplo clásico, útil para ilustrar (casi) todas las posibilidades comentadas
en las dos secciones precedentes:

Naval Fate.

Usage:
naval_fate ship new <name>...
naval_fate ship <name> move <x> <y> [--speed=<kn>]
naval_fate ship shoot <x> <y>
naval_fate mine (set|remove) <x> <y> [--moored|--drifting]
naval_fate -h | --help
naval_fate --version

Options:
-h --help Show this screen.
--version Show version.
--speed=<kn> Speed in knots [default: 10].
--moored Moored (anchored) mine.
--drifting Drifting mine.

El ejemplo muestra las opciones de un juego de barcos, en el que el usuario puede manejar barcos
(ship) o minas (mine). Los argumentos que no están rodeados por corchetes oblicuos son considerados
literales, y han de aparecer tal cual se escriben. En función de qué acción se desea realizar, pueden
usarse unas opciones u otras.
Por ejemplo, para crear el vapor el Català y el bajel pirata el Temido, y desplazar el primero a La
Habana (coordenadas x, y=100, 230) con una velocidad de 40 nudos, ejecutaríamos el programa con
los siguientes argumentos:

181
CAPÍTULO 10. SCRIPTS, MÓDULOS, ENTORNOS VIRTUALES Y PROGRAMAS.

usuario:~/naval$ naval_fate ship new el-catala el-temido


usuario:~/naval$ naval_fate ship el-catala move 100 230 --speed=40

[Link]. Nombre típico de los argumentos de las opciones en GNU.

En la sinopsis del ejemplo, el argumento de la opción '--speed' recibe el nombre '<kn>', abreviatura
de knot (nudo, en inglés). Está claro que se trata de un número, pero no se sabe si tiene que ser entero
o real. El valor por defecto, 10, tampoco ayuda mucho; ¿podemos usar un valor no entero?
Lo cierto es que usar el nombre '<kn>' no es de gran ayuda, ya que el significado del argumento aparece
en la descripción de la opción, y además en formato completo, no como una abreviatura que se puede
entender o no. Hubiera sido más interesante si se nos informara del tipo de argumento esperado.
El convenio usado por GNU en sus programas es muy recomendable: consiste en indicar el tipo esperado
del argumento usando una palabra en mayúsculas. Por ejemplo, en el caso de la velocidad del barco,
podríamos usar 'INT', 'REAL', 'FLOAT', etc.
Algunos nombres típicos usados por GNU son los siguientes, aunque no es necesario adherirse a ellos
de manera estricta, siempre que su significado quede claro al usuario:

'INT': Número entero. En ocasiones, se usa alguna expresión de carácter inequívocamente entero,
como 'SIZE' o 'LENGTH'.

'REAL': Número real. Otras posibilidades son 'FLOAT', 'DOUBLE', etc.

'CHAR': Un único carácter; a veces abreviado como 'CHR'. En ocasiones se puede indicar el tipo
concreto de carácter: 'LETTER', 'DIGIT', etc.

'STRING': Cadena de caracteres; a veces abreviado como 'STR'.


En ocasiones, se usa un nombre específico que indica el tipo de cadena esperado: 'WORD',
'SENTENCE', 'NAME', 'CITY', etc.

'FILE': Nombre de fichero. También se usa 'FILENAME'.

10.2.4. Gestión de opciones usando la biblioteca docopt.

Para facilitar la gestión del CLI se han desarrollado toda una serie de aplicaciones y funciones de
librería. Por ejemplo, durante mucho tiempo la única opción en programas C era la función getopt()
(ejecutar man 3 getopt para ver su funcionamiento).
El problema con estas soluciones es que todo el trabajo de gestión de las opciones y del mensaje con la
sintaxis recae sobre el programador. Eso quiere decir que es él el que debe garantizar que el mensaje
mostrado en pantalla se corresponde exactamente con la funcionalidad del programa. Por desgracia, es
habitual que las modificaciones en esta última no se reflejen adecuadamente en aquél. Así, es frecuente
encontrar en los mensajes de sinopsis gestionados de este modo opciones obsoletas (que ya no son
operativas o han cambiado su significado) u ocultas (que no se han incorporado al mensaje).
En el lenguaje Python, se introdujo la función optparse(), luego sustituida por argparse(). Además
de ser notoriamente más versátiles y potentes que el getopt() de C, la gran ventaja de optparse()
es que, a partir de la definición algorítmica de las opciones del programa, el mensaje de sinopsis se
genera automáticamente. No obstante, la gestión de las opciones continuaba siendo bastante farragosa
(aunque no tanto como con getopt()), y no proporciona demasiada flexibilidad en la confección del
mensaje de sinopsis.

182
10.2. CONSTRUCCIÓN DE PROGRAMAS EN PYTHON.

En 2012, Vladimir Keleshev ideó un sistema de gestión del CLI para programas escritos en Python muy
original: dado que todo programa debería tener un mensaje de sinopsis que, a su vez, sirve de docu-
mentación del código, construyamos un mecanismo que permita realizar el análisis de los argumentos
y opciones a partir del mismo. Esta estrategia tiene dos ventajas: permite mayor flexibilidad en la
construcción del mensaje de sinopsis y, sobre todo, la gestión algorítmica de las opciones se simplifica
al extremo.
En su versión inicial, esta idea se implementó como el módulo de Python docopt , aunque luego se ha
adaptado a otros lenguajes, como C/C++, Java, Ruby, etc.
Puede encontrar más información acerca de docopt, su modo de empleo y todas sus opciones en su
repositorio de GitHub .

[Link]. Sintaxis de la sinopsis para su uso con docopt.

Para ser interpretado correctamente por docopt, el mensaje con la sinopsis debe cumplir una serie de
condiciones. Tomaremos el ejemplo en la página 181 para explicar cada una de ellas:

Las primeras líneas son opcionales y sirven para describir, en pocas palabras, el cometido del
programa. El estándar POSIX recomienda que esta descripción sea lo más concisa posible y que
no repita innecesariamente el nombre del programa.
En el ejemplo, la primera línea es, simplemente, 'Naval Fate'.

Una sección de modo de empleo encabezada por la cadena 'Usage:' (sin importar minúsculas y
mayúsculas) y rematada por una línea en blanco.

• La sección puede contener tantas líneas de modo de empleo como modos distintos permita
el programa. Cada modo de empleo debe empezar por el nombre del programa seguido por
las opciones y argumentos que lo distinguen del resto de modos.
En el ejemplo, tenemos modos de empleo diferentes para la creación de barcos, su movi-
miento, su disparo, etc.
◦ Los modos especiales -h | --help y --version muestran el mensaje de sinopsis y la
versión de programa, respectivamente, y finalizan la ejecución. En principio, de esta
gestión se encarga el propio docopt.
◦ Habitualmente, los programas sólo tendrán un modo de empleo principal. Sin embargo,
es conveniente mostrar las opciones -h | --help y --version como modos de empleo
diferenciados, ya que es la información que recibirá en primera instancia el usuario si
invoca el programa con opciones o argumentos incorrectos.
• Cada modo de empleo debe incluir todos sus argumentos posicionales, indicando su carácter
opcional, obligatorio, alternativo o repetitivo conforme a lo indicado en el apartado [Link].
• Las opciones del programa también pueden indicarse en el modo de empleo. Aunque las
que son realmente opcionales pueden agruparse en la cadena '[options]' y posponer su
descripción hasta la sección 'Options' de la sinopsis.

Una sección de descripción de las opciones encabezada por la cadena Options: (o cualquier otro
nombre).

• Todas las líneas de la sinopsis que empiecen por guion (sin tener en cuenta espacios o
tabuladores), son consideradas el inicio de la descripción de una opción. Esta descripción
puede abarcar más de una línea, incluyendo líneas en blanco.
• Todas las opciones del programa pueden ser descritas en esta sección, aunque no todas
deben estarlo.
En los casos siguientes, la inclusión de la opción sí es necesaria:

183
CAPÍTULO 10. SCRIPTS, MÓDULOS, ENTORNOS VIRTUALES Y PROGRAMAS.

◦ Si la opción está englobada en la cadena '[options]' del modo de empleo. Es decir, si


no se incluye explícitamente en los modos de empleo.
◦ Si se desea dar un valor por defecto al argumento de la opción. En ese caso, el valor se
incluirá en la descripción en la forma '[default: valor-por-defecto]'
• Si una opción puede escribirse en su forma corta o larga, ambas se escribirán en la misma
línea, separadas por un único espacio o una coma.
• Entre la opción, junto a su posible argumento, y la descripción debe haber un mínimo de
dos caracteres en blanco.
Estos dos espacios en blanco son muy importantes, ya que el mecanismo usado por docopt
para separar la opción de su descripción se basa en ellos. Si no colocamos dos espacios entre
ambas, docopt se armará un lío; si colocamos dos espacios en medio de la propia opción
(por ejemplo, entre la opción y su argumento, o entre la opción larga y la corta), también.

Una sección opcional final de formato libre y que se puede usar para describir el funcionamiento
del programa, su autoría y/o copyright o cualquier otra cosa que se considere de interés.
Es conveniente, por ejemplo, incluir una sección de nombre 'Argumentos:', o semejante, donde
se explique el significado de los argumentos posicionales del programa.

[Link]. Análisis sintáctico de la invocación.

En Python, el análisis de la invocación se realiza con la función [Link], cuya sintaxis es:

[Link](sinopsis, argv=None, help=True, version=None, options_first=False)

Donde:

sinopsis: Cadena de texto con el mensaje de la sinopsis. A menudo, es el docstring del fichero
(__doc__), aunque puede ser cualquier otra cadena.
Es el único argumento no inicializado y, por tanto, obligatorio.

argv: Argumentos a analizar en la forma de lista de cadenas de texto. Por ejemplo, en el caso
de Naval Fate podríamos pasar argv=['ship', 'new', 'el-catala'] para crear el barco a
vapor El Català.
Si se usa la opción por defecto, argv=None, se emplea la lista de argumentos usados en la
invocación del programa ([Link][1:]).

help=True: Indica a la función que, en caso de encontrar la opción '-h' o '--help', debe mostrar el
mensaje de sinopsis en pantalla y finalizar la ejecución.
Si se prefiere realizar la gestión de la ayuda de otro modo, deberá especificarse help=False.

version=None:
Objeto imprimible, habitualmente una cadena de texto, que se mostrará en pantalla si se
ejecuta el programa con la opción '--version'. Si es distinta de None, docopt() lo mostrará
en pantalla y finalizará la ejecución.

options_first=False:
Indica si el programa debe seguir el criterio POSIX estricto que requiere que las opciones
vayan antes de cualquier argumento posicional.

Al llamar al programa desde el shell, lo primero que hace docopt es analizar sintácticamente la orden
invocada para determinar si es correcta o si se han usado las opciones especiales --help o --version.

184
10.2. CONSTRUCCIÓN DE PROGRAMAS EN PYTHON.

Para que docopt() maneje estas opciones especiales, además de su uso en la invocación de la función,
deberán aparecer listadas en el mensaje de sinopsis.
Si la invocación incluye la opción --help, docopt() mostrará el mensaje de sinopsis entero y acabará
la ejecución del programa:

usuario:~/naval$ naval_fate --help


Naval Fate.

Usage:
naval_fate ship new <name>...
naval_fate ship <name> move <x> <y> [--speed=<kn>]
naval_fate ship shoot <x> <y>
naval_fate mine (set|remove) <x> <y> [--moored|--drifting]
naval_fate -h | --help
naval_fate --version

Options:
-h --help Show this screen.
--version Show version.
--speed=<kn> Speed in knots [default: 10].
--moored Moored (anchored) mine.
--drifting Drifting mine.

Si la invocación incluye la opción --versión (y no incluye la --help), se muestra la cadena de texto


pasada a la función [Link]() como argumento version. Por ejemplo, si al invocar a la función
desde el programa, se hubiera hecho con la opción [Link](..., version='Naval Fate v1.0',
...), el resultado sería:

usuario:~/naval$ naval_fate --version


Naval Fate v1.0

Si la invocación no incluye ninguna de las dos opciones especiales, pero tiene algún error de sintaxis,
docopt muestra los modos de empleo enumerados en la sección 'Usage'.
Típicamente, aunque no siempre, los programas requieren de una opción o argumento como mínimo.
Así que invocar el nombre del programa a secas constituye un error que provocará que se muestre el
modo o modos de empleo correctos. Ésta es una manera muy usada de ver cómo hay que invocar un
programa (aunque, a veces, se nos puede quedar cara de tonto al hacerlo; pruebe ejecutando cat o
echo sin ningún argumento para ver ejemplos de ello):

usuario:~/naval$ naval_fate
Usage:
naval_fate ship new <name>...
naval_fate ship <name> move <x> <y> [--speed=<kn>]
naval_fate ship shoot <x> <y>
naval_fate mine (set|remove) <x> <y> [--moored|--drifting]
naval_fate -h | --help
naval_fate --version

Finalmente, si no hay ningún error sintáctico y no se han usado las opciones especiales --help o
--version, docopt() no mostrará nada, analizará el contenido de opciones y argumentos y proseguirá
la ejecución del programa.

[Link]. Acceso a los argumentos desde el programa.

Una vez escrito el mensaje de sinopsis tal cual se desea que aparezca en pantalla, éste se pasa la función
docopt() que, al ejecutar el programa, devolverá un diccionario en el que las claves son los nombres

185
CAPÍTULO 10. SCRIPTS, MÓDULOS, ENTORNOS VIRTUALES Y PROGRAMAS.

de los argumentos u opciones (en su versión larga, si ésta existe), y los valores son las cadenas de
texto pasadas como argumento (que deberán interpretarse y convertirse al formato deseado usando
las funciones apropiadas).
En el diccionario devuelto por docopt() los valores almacenados tienen el tipo siguiente:

Comandos y flags simples.


Los comandos y flags que sólo pueden aparecer una vez se almacenan como variables de tipo
bool, que puede tomar el valor True si son nombrados en la línea de comandos, o False, si no
lo son.

Comandos y flags repetibles.


Los comandos y flags que pueden aparecer más de una vez toman valores enteros que indican
cuántas veces lo hacen.

Argumentos repetibles.
Si un argumento puede ser repetido, los distintos valores se almacenan en una lista de cadenas
de texto. Si no se indica el argumento, la lista estará vacía; si sólo se indica una vez, la lista
sólo tendrá un elemento. En todos los casos, el valor devuelto es una lista.

Resto de argumentos.
En el resto de casos, el valor del argumento se pasa como una cadena de texto, tal cual se
escribió en la invocación, o como el objeto None, si no se escribió su valor.
Es responsabilidad del programador el comprobar si el argumento es None o una cadena de
texto y, en este último caso, interpretar correctamente su valor.

Valores por defecto.


En argumentos repetibles, el valor por defecto se interpretará como una lista de palabras
separadas por espacios, incluso en el caso en que haya una sola palabra.
En argumentos que sólo pueden aparecer una vez, se interpretará como una única cadena de
texto, aún en el caso de que esté formada por distintas palabras.
En comandos y flags no tiene sentido usar valores por defecto.

Por ejemplo, el código siguiente analizaría el mensaje de sinopsis en la página 181 y, en el caso de
que la acción fuera move, extraería el nombre del barco (argumento <name>) y el valor de la velocidad
(argumento de la opción --speed):

from docopt import docopt

if __name__ == '__main__':
arguments = docopt(sinopsis, version='Naval Fate 2.0')

if arguments['move']:
name = arguments['<name>'][0]
speed = float(arguments['--speed']) if arguments['--speed'] else None

...

Fijémonos en que, al acceder al diccionario devuelto por docopt(), las claves han de pasarse exacta-
mente como se han escrito en la sinopsis, incluyendo los corchetes triangulares ('<name>') o los guiones
('--speed').
En el caso de arguments['<name>'], como en el comando 'new' el número de nombres es variable, su
valor se pasa como lista de cadenas de texto, aunque en el propio comando move se trate de sólo uno.

186
10.2. CONSTRUCCIÓN DE PROGRAMAS EN PYTHON.

El comportamiento de docopt en estos casos es devolver el caso más general posible, que es la lista de
cadenas. Por tanto, debemos acceder al primer (y único) elemento de la lista4 .
La velocidad, argumento de --speed nunca puede ser None, ya que en la sinopsis se le da el valor por
defecto speed = 10. No obstante, es conveniente habituarse a comprobar el valor de los argumentos
antes de pasárselos a una función como el constructor float(), ya que, en caso de que fuera None se
produciría un error.
Nótese también la necesidad de convertir el argumento de la opción --speed a real; los valores devueltos
por docopt son de alguno de los tipos enumerados en la página anterior, en general, cadenas de texto
o listas de cadenas. Es responsabilidad del programador convertirlos al tipo deseado.

[Link]. Ejemplo completo de Naval Fate.

El código completo de una versión del programa Naval Fate podría tener la forma siguiente:

1 #! /usr/bin/python3
2
3 import sys
4 from docopt import docopt
5
6 def crea_barco(name):
7 print(f'Cremos el barco "{name}"')
8
9 def mueve_barco(name, x, y, speed):
10 print(f'Enviamos el barco "{name}" a las coordenadas {x=}, {y=} ', end='')
11 print(f'con una velocidad {speed=} nudos')
12
13 def dispara(x, y):
14 print(f'Disparamos a las coordenadas {x=}, {y=}')
15
16 def mina(x, y, set_, moored, drifting):
17 print(f'{"Colocamos" if set_ else "Retiramos"} una mina', end='')
18 print(f'{" fija" if moored else " a la deriva" if drifting else ""}', end='')
19 print(f' en las coordenadas {x=}, {y=}')
20
21
22 sinopsis = f"""
23 Naval Fate.
24
25 Usage:
26 {[Link][0]} ship new <name>...
27 {[Link][0]} ship <name> move <x> <y> [--speed=<kn>]
28 {[Link][0]} ship shoot <x> <y>
29 {[Link][0]} mine (set|remove) <x> <y> [--moored|--drifting]
30 {[Link][0]} -h | --help
31 {[Link][0]} --version
32
33 Options:
34 -h --help Show this screen.
35 --version Show version.
36 --speed=<kn> Speed in knots [default: 10].
37 --moored Moored (anchored) mine.
38 --drifting Drifting mine.
39 """
40
41 if __name__ == '__main__':
42 args = docopt(sinopsis, version='Naval Fate 2.0')

4
Este es un caso un poco especial, en el que el comportamiento de docopt es completamente arbitrario. Existirían
otros posibles comportamientos, como que se usara una lista de cadenas en un caso y una cadena en el otro, o que se
produjera un error. En general, no será necesario tener en cuenta estos detalles.

187
CAPÍTULO 10. SCRIPTS, MÓDULOS, ENTORNOS VIRTUALES Y PROGRAMAS.

43
44 # Si se han indicado las coordenadas <x> e <y>, usadas en tres de los
45 # cuatro modos, las convertimos a real.
46 try:
47 x = float(args['<x>']) if args['<x>'] else -1
48 y = float(args['<y>']) if args['<y>'] else -1
49 except:
50 print('Las coordenadas "<x>" e "<y>" deben ser números reales')
51 exit(-1)
52
53 if args['ship']:
54 if args['new']:
55 for name in args['<name>']:
56 crea_barco(name)
57 elif args['move']:
58 try:
59 speed = float(args['--speed'])
60 except:
61 print('La velocidad debe ser un número real')
62 exit(-1)
63
64 mueve_barco(args['<name>'][0], x, y, speed)
65 elif args['shoot']:
66 dispara(x, y)
67 elif args['mine']:
68 # Tal y como se ha escrito la sinopsis, 'set' y 'remove' son mutuamente
69 # excluyentes y se ha de indicar uno u otro. Por tanto, si 'set' es True,
70 # 'remove' es False y viceversa.
71 #
72 # En el caso de '--moored' y '--drifting', también son mutuamente
73 # excluyentes, pero no son obligatorios. Por tanto, los dos podrían ser
74 # False simultáneamente. Seguramente es un error del autor del paquete,
75 # porque mucho sentido no tiene.
76 mina(x, y, set_=args['set'], moored=args['--moored'], drifting=args['--drifting'])

10.2.5. Más información de docopt.

La documentación completa de docopt, con una explicación de su uso, la descripción de todas sus opcio-
nes y varios ejemplos muy ilustrativos se encuentra en su repositorio de GitHub . Es muy recomendable
leer la información en él contenida.

10.3. Paquetes y módulos.

A menudo resulta conveniente agrupar módulos que comparten alguna característica u objetivo. Del
mismo modo, también puede convenir dividir un módulo de gran tamaño en partes más pequeñas que
resulten más manejables. El mecanismo que proporciona Python para conseguir esto es el paquete. Un
paquete es una organización jerárquica de módulos y otros paquetes, y que puede ser importada de
manera conjunta.
Aunque hay casos en que la analogía no es del todo cierta, es conveniente pensar en los paquetes como
una estructura de directorios en la que los módulos son los scripts que contiene. De hecho, este es el
modo como se organizan los paquetes típicos.
Por ejemplo, podemos pensar en un paquete matemático básico que incluyera herramientas de álgebra y
cálculo. La organización del susodicho paquete podría responder a la siguiente estructura de directorios
y archivos:

188
10.3. PAQUETES Y MÓDULOS.

mates
cálculo
numérico
[Link]
[Link]
simbólico
[Link]
[Link]
álgebra
[Link]
[Link]
[Link]
[Link]

Sin embargo, una estructura de directorios no basta para definir un paquete. Podemos importar el
directorio mates, pero eso no importará ninguna definición contenida en los scripts que cuelgan de él;
lo cual podemos comprobar con el comando dir():

>>> import mates


>>> dir(mates)
['__doc__', '__file__', '__loader__', '__name__', '__package__', '__path__', '__spec__']

10.3.1. El fichero __init__.py.

Para que un directorio se comporte como un paquete, es necesario que contenga el fichero __init__.py.
El resultado de la importación será, precisamente, la ejecución de este fichero. De hecho, antes de
Python 3, un directorio, o cualquiera de los elementos contenidos en él, sólo podía ser importado
si contenía el fichero __init__.py. A partir de Python 3, el fichero puede estar ausente, lo cual es
equivalente a tener uno que está vacío.
Las funciones del fichero __init__.py son dos:

Ejecución del código contenido en el fichero:


• A menudo realiza tareas de inicialización del paquete, como la definición de nombres globales
que serán usados por los elementos que lo forman.
• Realiza la importación de los otros subpaquetes y/o módulos que forman el paquete.
Definición de la variable __all__, que debe ser una lista de los identificadores efectivamente
importados al ejecutar from paquete import *.
Por defecto, Python incluirá en el espacio de nombres todos los objetos definidos en __init__.py o
en los módulos por él importados, salvo aquéllos que comiencen por subrayado ('_'). Si existe la
lista __all__, sólo los nombres incluidos en ella, empiecen o no por subrayado, serán importados.

Por ejemplo, en el paquete matemático, el fichero mates/__init__.py podría tener el código siguiente:
mates/__init__.py
1 """
2 Paquete matemático básico. Úsese con moderación.
3 """
4
5 copyright = '(c) UPC, 2021'
6 version = '3.17'
7
8 import mates.álgebra, mates.cálculo

189
CAPÍTULO 10. SCRIPTS, MÓDULOS, ENTORNOS VIRTUALES Y PROGRAMAS.

Fijémonos en que el nombre del paquete a importar, álgebra o cálculo, no basta para que Python
sea capaz de localizarlo; es necesario indicar también el paquete del que cuelgan, mates.álgebra y
mates.cálculo.
Ahora, el resultado de importar el paquete mates incluye los nombres definidos en mates/__init__.py,
así como los paquetes importados por él:

>>> import mates


>>> dir(mates)
['__builtins__', '__cached__', '__doc__', '__file__', '__loader__', '__name__', ···
,→ '__package__', '__path__', '__spec__', 'copyright', 'cálculo', 'mates', 'version', ···
,→ 'álgebra']

Además, la cadena inicial se guarda en el docstring del paquete; es decir, en su atributo __doc__, y
puede ser accedida al solicitar ayuda sobre el módulo:

>>> help(mates)

NAME
mates - Paquete matemático básico. Úsese con moderación.

PACKAGE CONTENTS

SUBMODULES
cálculo
álgebra

DATA
copyright = '(c) UPC, 2021'
version = '3.17'

FILE
/home/albino/APAV/mates/__init__.py

De todos modos, el hecho de importar los subpaquetes mates.álgebra y mates.cálculo no implica que
los módulos contenidos en ellos se importen automáticamente. Lo podemos comprobar con el comando
dir():

>>> dir(mates.álgebra)
['__doc__', '__file__', '__loader__', '__name__', '__package__', '__path__', '__spec__']

Nuevamente, la importación del directorio sólo importa su nombre, y, nuevamente, si queremos hacer
algo de más utilidad, hemos de escribir un __init__.py en cada uno de ellos.
En el caso de mates.cálculo, el fichero __init__.py deberá incluir los subpaquetes que cuelgan de él,
mates.cá[Link]érico y mates.cá[Link]ólico, que, para hacer algo de utilidad, deberán disponer
de sus propios __init__.py:
mates/cálculo/__init__.py
1 import mates.cá[Link]érico
2 import mates.cá[Link]ólico

El caso de mates.álgebra es ligeramente distinto, porque ahora queremos importar los módulos conteni-
dos en el directorio mates/álgebra; es decir, [Link], [Link], [Link] y [Link]. Tí-
picamente, querremos que las funciones y objetos en ellos definidos se importen directamente al espacio

190
10.3. PAQUETES Y MÓDULOS.

de nombres del paquete mates.álgebra. Si no lo hacemos así, para referenciar una función deberemos en-
cadenar toda la retahíla de nombres de paquetes y módulos. Por ejemplo, mates.á[Link].
Para evitarlo, y reducir el nombre de la clase a mates.á[Link], podemos emplear la notación
from módulo import *:
mates/álgebra/__init__.py
1 from mates.á[Link] import *
2 from mates.á[Link] import *
3 from mates.á[Link] import *
4 from mates.á[Link] import *

Ahora, importando mates, tenemos acceso a los elementos definidos en mates/álgebra/[Link]:

>>> import mates


>>> mates.á[Link]([1, 2, 3])
Vector([1, 2, 3])

10.3.2. Referencias absoluta y relativa a paquetes y módulos.

Se ha visto en el apartado precedente que para importar un subpaquete desde el paquete que lo
contiene es necesario indicar su ruta completa. Así, el fichero __init__.py del directorio mates debe
importar mates.álgebra y mates.cálculo. Es decir, no es suficiente con importar álgebra y cálculo
aunque se haga desde el directorio mates, sino que hay que indicar su posición concreta dentro del
paquete. Es lo que se llama referencia absoluta.
Usando la forma from paquete import módulo es posible usar referencia relativa al directorio en el
que se ubica el fichero. En ellas, el directorio actual se indica con un punto ('.'). Así, en el fichero
mates/cálculo/__init__.py, en lugar de expresiones del tipo from mates.cálculo import módulo, pode-
mos usar la forma más compacta from . import módulo:
mates/cálculo/__init__.py (alternativa 1 )
1 from . import numérico, simbólico

De hecho, es aún más compacto y manejable importar todos los elementos del paquete, indicando en
la variable __all__ los módulos concretos que deseamos:
mates/cálculo/__init__.py (alternativa 2 )
1 __all__ = ['numérico', 'simbólico']
2
3 from . import *

Esta sintaxis, usando referencias relativas, puede usarse también para importar los elementos definidos
en el interior de un script. En este caso, indicamos un fichero contenido en el directorio actual enlazando
un punto con el nombre del fichero sin extensión:
mates/álgebra/__init__.py
1 from .ecuaciones import *
2 from .matrices import *
3 from .tensores import *
4 from .vectores import *

Finalmente, es frecuente necesitar definiciones presentes en un módulo de otra rama del paquete. Tam-
bién en este caso es posible realizar referencias relativas, retrocediendo en la estructura de directorios
añadiendo puntos al inicio y avanzando añadiendo sus nombres separados por punto. Por ejemplo, en
el módulo mates/cálculo/numérico/[Link] podríamos necesitar la definición de Vector presente en
mates/álgebra/[Link]. Esto se consigue añadiendo la siguiente orden al inicio del módulo:

191
CAPÍTULO 10. SCRIPTS, MÓDULOS, ENTORNOS VIRTUALES Y PROGRAMAS.

mates/cálculo/numérico/[Link]
1 from ...á[Link] import *

10.3.3. Directorios de búsqueda de los paquetes y módulos.

Al realizar una importación, en cualquiera de sus formas, en un script de Python, el intérprete intenta
localizar el módulo correspondiente en tres sitios distintos. Usará el módulo que encuentre primero y,
si no lo encuentra en ninguno de los tres, elevará la excepción ModuleNotFoundError:

El directorio desde el que se ejecuta el intérprete, en el caso de ejecución interactiva, o en el que


se ubica el script, en el caso de ejecutar uno.

Los directorios indicados por la variable de entorno $PYTHONPATH. Esta variable tiene la misma
sintaxis que la $PATH; es decir, una lista de nombres de directorio separados por dos puntos.

Los directorios específicos de la versión de Python instalada en el sistema. Esta lista puede
incluir los paquetes instalados por el usuario en su propia zona de usuario y/o los instalados en
un entorno virtual.

Al iniciarse la ejecución de un módulo (o la interactiva, si es el caso), el intérprete organiza una lista


de directorios de búsqueda a partir de estos tres sitios. La lista, de nombre [Link], siempre empieza
con el directorio '', que es el directorio actual. A continuación se colocan los directorios listados en
$PYTHONPATH y, finalmente, los específicos del sistema. Al intentar importarse un módulo, éste se buscará
en ese orden:

>>> import sys


>>> [Link]
['', '/home/albino/TecParla/apuntes/ramses', '/usr/lib/[Link]', ···
,→ '/usr/lib/python3.8', '/usr/lib/python3.8/lib-dynload', ···
,→ '/home/albino/.local/lib/python3.8/site-packages', ···
,→ '/usr/local/lib/python3.8/dist-packages', '/usr/lib/python3/dist-packages']

En este caso, por ejemplo, el usuario tiene la orden export PYTHONPATH+=~/TexParla/apuntes/ramses en


su fichero ~/.bashrc, lo cual le permite importar los paquetes ubicados en ese directorio desde cualquier
lugar del sistema.
La lista [Link] es mutable y podemos variar su contenido en tiempo de ejecución. Por ejemplo, si
eliminamos el primer elemento (''), que indica el directorio actual, éste dejará de participar en la
búsqueda:

>>> import sys


>>> [Link] = [Link][1:] # Eliminamos '' de la lista de directorios

>>> import mates


Traceback (most recent call last):
File "<stdin>", line 1, in <module>
ModuleNotFoundError: No module named 'mates'

>>> [Link]('') # Restituimos '' a la lista


>>> import mates # Ahora, la importación se hace correctamente

192
10.4. ENTORNOS VIRTUALES.

10.3.4. Si todo esto le parece un follón...


Es probable que al lector le haya parecido que la gestión de paquetes y módulos es poco clara. Si no es
así, es muy probable que sí se lo parezca cuando intente poner en funcionamiento un paquete mínima-
mente complejo. No se trata de un problema de comprensión lectora. En todo caso, de poca claridad en
la explicación, que puede complementar acudiendo a the import system o the Python tutorial: Modules ,
de los pythonisos de [Link], o a este tutorial de la gente de RealPython .
Uno de los motivos por los que el sistema de módulos y paquetes de Python puede parecer más sencillo
de lo que realmente es es el hecho de que siempre se explica, incluyendo en este mismo texto, usando
un mismo directorio para todo: el programa se ejecuta en el mismo directorio en que se encuentra él y
el paquete. Lo habitual, sin embargo, es que coloquemos las bibliotecas en un directorio, los programas
ejecutables en otro y ejecutemos el programa desde otro. Si lo hace así, el programa será incapaz de
localizar el paquete.
En este caso, lo peor que puede hacer es modificar los imports que puedan aparecer en el programa o los
distintos módulos del paquete. Tenga en cuenta que el paquete ya funcionaba correctamente cuando
todo se hacía desde el mismo directorio; si modifica los imports es muy probable que ya no funcione
desde allí. La solución más oportuna es sencilla: modifique las variables de entorno $PYTHONPATH y $PATH
para que el directorio del que cuelga el paquete aparezca en la primera, y el directorio de los programas
en la segunda.
En cualquier caso, no sirve de mucho desesperarse: el sistema de importación de Python es realmente
potente y más sencillo de lo que parece; tras unos pocos ajustes (en ocasiones, no tan pocos), es posible
ajustarse a todo tipo de situaciones. Además, puede costar un poco poner en marcha un paquete, pero,
una vez puesto, sólo serán necesarios pequeños retoques para que funcione sin problemas durante todo
el desarrollo y empleo del mismo.

10.4. Entornos virtuales.


Uno de los mayores atractivos de Python es la abundancia de paquetes desarrollados para él. Cientos
(o miles) de desarrolladores del todo el mundo están constantemente produciendo y publicando nuevos
paquetes, o mejorando otros ya existentes. Para hacernos una idea, en un soleado día de mayo de 2021,
el número de paquetes publicados en el repositorio oficial de Python, PyPI (Python Package Index),
era 306 860 a las nueve y media de la mañana; al día siguiente, a la misma hora, el número había
subido a 307 102. Es decir, en un día se habían publicado 242 nuevos paquetes, a una media de unos
diez por hora.
Un problema con todos estos paquetes es que no son estáticos, sino que suelen sufrir varias modifica-
ciones a lo largo de su vida. El problema aparece cuando tenemos distintos programas que dependen
de versiones distintas de un mismo paquete y son incompatibles entre sí. Incluso, a veces, la incom-
patibilidad puede aparecer con un determinado intérprete o entorno de desarrollo. Por ejemplo, en
los cursos de Python anteriores a 2018 se utilizaba el entorno de desarrollo PyCharm que, cuando
se intentaba usar las últimas versiones de matplotlib petaba con un bonito Segmentation Fault (core
dumped). La solución pasaba por instalar una versión más antigua (en concreto, la 2.2.0).
El problema tiene dos vertientes:

Por un lado, una misma versión de un paquete, probablemente la más moderna, no tiene por qué
funcionar correctamente con todos los programas. Eso imposibilita tener una única versión system
wide, para todos ellos. La versión por defecto del sistema funcionará con algunos programa, pero
no con otros o incluso con ninguno.
Por otro lado, poner todos los paquetes a punto, con las versiones adecuadas para un cierto
programa, puede suponer un esfuerzo elevado que, seguramente, no estaremos dispuestos a poner
en peligro porque a algún lumbreras se le haya ocurrido mejorar uno de los paquetes de los que
depende.

193
CAPÍTULO 10. SCRIPTS, MÓDULOS, ENTORNOS VIRTUALES Y PROGRAMAS.

La solución consiste en los llamados entornos virtuales. Un entorno virtual consiste, básicamente, en
la creación de un conjunto de herramientas que, cuando se activa el entorno, sustituye al intérprete
de Python del sistema, las herramientas (como pip) asociadas al mismo y los paquetes instalados.

10.4.1. Entornos virtuales con venv.

En Python estándar, la herramienta de creación de entornos virtuales es el módulo d e la biblioteca


estándar de Python. El módulo se ejecuta desde la línea de comandos con la opción -m para permitir
seleccionar la versión concreta del intérprete de Python que deseamos usar. Es posible que, para poder
usar el módulo, deba instalarlo antes. En Ubuntu se instala con la orden siguiente de apt5 :

usuario:~/APAV$ sudo apt install python3-venv

Una vez instalado, puede ejecutar python3.8 -m venv sin más opciones para ver el modo de empleo, o
python3.8 -m venv --help para ver una descripción detallada de las opciones:

usuario:~/APAV$ python3.8 -m venv --help


usage: venv [-h] [--system-site-packages] [--symlinks | --copies] [--clear]
[--upgrade] [--without-pip] [--prompt PROMPT] ENV_DIR [ENV_DIR ...]

Creates virtual Python environments in one or more target directories.

positional arguments:
ENV_DIR A directory to create the environment in.

optional arguments:
-h, --help show this help message and exit
--system-site-packages Give the virtual environment access to the system
site-packages dir.
--symlinks Try to use symlinks rather than copies, when symlinks
are not the default for the platform.
--copies Try to use copies rather than symlinks, even when
symlinks are the default for the platform.
--clear Delete the contents of the environment directory if
it already exists, before environment creation.
--upgrade Upgrade the environment directory to use this version
of Python, assuming Python has been upgraded in-place.
--without-pip Skips installing or upgrading pip in the virtual
environment (pip is bootstrapped by default)
--prompt PROMPT Provides an alternative prompt prefix for this
environment.

Once an environment has been created, you may wish to activate it, e.g. by
sourcing an activate script in its bin directory.

Especificación de la versión de Python al ejecutar venv.

Al instalar un intérprete de Python en un sistema, lo que se instala es un programa cuyo nombre


es python seguido de la versión del lenguaje (en general, 2 o 3) y del número de edición mayor
(por ejemplo, 8 o 9). Así, en la primavera de 2021 lo habitual es, si se instala la última versión
estable del intérprete, instalar el programa python3.9.

Además, al instalar python3.9, también se crea un enlace simbólico de nombre python3 que

5
Seguramente, en MacOS se tenga que usar una orden semejante, pero usando brew en lugar de apt.

194
10.4. ENTORNOS VIRTUALES.

apunta al ejecutable, de manera que, al invocar python3, lo que realmente estamos ejecutando
es python3.8. Puede comprobarlo ejecutando ls -l $DIR_BIN/python*, donde $DIR_BIN es el di-
rectorio en el que se alojan los programasa .

Un problema de este planteamiento es que, si instalamos una versión más reciente de Python,
python3 dejará de apuntar a python3.9 para apuntar a la nueva. Es decir, aunque podemos tener
dos o más versiones distintas de Python, el nombre python3 sólo apuntará a una de ellas; en
general, la más reciente.

Durante la creación del entorno virtual, venv creará enlaces simbólicos al intérprete usado en
la invocación. Por este motivo es muy importante invocarlo usando Python con su nombre
completo, incluyendo el número de versión. De este modo, el entorno virtual ejecutará siempre
la versión concreta de Python para la que fue creado, aunque se instale una versión más nueva
en el sistema.
a
Puede determinar el directorio donde se instala Python ejecutando el comando which python3. En sistemas
Ubuntu, el directorio suele ser /usr/bin.

Como puede verse, usando las opciones por defecto, que en general serán apropiadas para la mayor
parte de aplicaciones, el único argumento que se le ha de pasar al programa es el nombre de un
directorio en el que se creará el entorno virtual. Por ejemplo, creamos el entorno entorno:

usuario:~/APAV$ python3.8 -m venv entorno

Al cabo de unos segundos, el programa termina sin ningún mensaje. Sin embargo, durante ese tiempo
ha creado una estructura de archivos de nombre entorno. Podemos ver su contenido con el programa
tree, con las opciones -d, para que sólo muestre directorios, y -L 2, para que sólo muestre los dos
primeros niveles de directorio:

usuario:~/APAV$ tree -d -L 2 entorno


entorno
bin
include
lib
python3.8
lib64 -> lib
share
python-wheels

De todos estos directorios, el más importante es entorno/bin, que es el que incluye los programas que
se usarán al activar el entorno virtual:

usuario:~/APAV$ ls -l entorno/bin/
total 44
-rw-r--r-- 1 albino albino 8834 May 27 00:29 Activate.ps1
-rw-r--r-- 1 albino albino 2211 May 27 00:29 activate
-rw-r--r-- 1 albino albino 1263 May 27 00:29 [Link]
-rw-r--r-- 1 albino albino 2415 May 27 00:29 [Link]
-rwxr-xr-x 1 albino albino 253 May 27 00:29 easy_install
-rwxr-xr-x 1 albino albino 253 May 27 00:29 easy_install-3.8
-rwxr-xr-x 1 albino albino 244 May 27 00:29 pip
-rwxr-xr-x 1 albino albino 244 May 27 00:29 pip3
-rwxr-xr-x 1 albino albino 244 May 27 00:29 pip3.8
lrwxrwxrwx 1 albino albino 9 May 27 00:29 python -> python3.8

195
CAPÍTULO 10. SCRIPTS, MÓDULOS, ENTORNOS VIRTUALES Y PROGRAMAS.

lrwxrwxrwx 1 albino albino 9 May 27 00:29 python3 -> python3.8


lrwxrwxrwx 1 albino albino 18 May 27 00:29 python3.8 -> /usr/bin/python3.8

En el directorio encontramos los siguientes elementos más destacados:

activate: Script que activa el entorno virtual. Como en él se redefinen variables de entorno, como
el $PATH o el prompt, es imprescindible ejecutar este script con source, no directamente.
El script activate está diseñado para el shell bash. En el caso de que el shell sea csh o fish,
el script a usar es el que tiene su nombre como extensión. En el caso de zsh, distribuido en
las últimas versiones de MacOS, es muy probable que el que haya que usar sea el mismo
que para bash.
Al ejecutar la activación, activate también define la función deactivate, que se usará para
desactivar el entorno virtual.
python3.8: Enlace simbólico al intérprete usado al invocar venv. Una vez activado el entorno, éste
será el intérprete que se usará.
Además, se generan enlaces simbólicos para python y python3, que apuntan a python3.8,
para que al invocar al intérprete con estos nombres también se ejecute el original.
pip3.8: Sustituto del pip3.8 del sistema, usado para que los paquetes que instalemos lo hagan en
el entorno virtual en lugar de los directorios del sistema o del usuario.
Como en el caso de python3.8, también se crean pip y pip3, para que cualquiera de los
tres nombres se comporten del mismo modo.

Al activar el entorno, el primer efecto que percibimos es el cambio del prompt, que pasa a indicarnos
que estamos ejecutando un entorno virtual:

usuario:~/APAV$ source entorno/bin/activate


(entorno)_usuario:~/APAV$

Además de por el cambio de prompt, podemos comprobar que se ha activado el entorno mirando qué
intérprete se va a ejecutar al invocar python3:

(entorno)_usuario:~/APAV$ which python3


/home/usuario/APAV/entorno/bin/python3

También notamos que algo raro pasa cuando intentamos ejecutar ipython3. Al hacerlo, se nos presenta
el siguiente mensaje:

(entorno)_usuario:~/APAV$ ipython3
/usr/lib/python3/dist-packages/IPython/core/[Link]:
UserWarning: Attempting to work in a virtualenv. If you encounter problems,
please install IPython inside the virtualenv.

Lo mejor que podemos hacer, si queremos usar ipython3, es hacer caso del mensaje e instalarlo desde
el entorno virtual con la orden pip3 install ipython. Sin embargo, esto nos obligará a usar ipython
(sin el 3), ya que, aunque se instala un ipython3 en el entorno virtual, éste continúa accediendo al
intérprete del sistema y no al del entorno (probablemente es un bug).
Finalmente, si queremos usar bibliotecas ya instaladas en el sistema, como numpy, nos encontraremos
con que no se encuentran. Lo que debemos hacer es instalarlas de nuevo. Al activar el entorno virtual,
es como si tuviéramos un sistema nuevo de trinca, con todos los paquetes por instalar:

196
10.4. ENTORNOS VIRTUALES.

>>> import numpy


Traceback (most recent call last):
File "<stdin>", line 1, in <module>
ModuleNotFoundError: No module named 'numpy'

Instalamos numpy usando pip, como lo haríamos para instalarlo en el sistema:

(entorno)_usuario:~/APAV$ pip install numpy

Del mismo modo, deberemos instalar todos los paquetes que queramos usar desde el entorno virtual.
Salimos del entorno virtual ejecutando la función deactivate, que definió entorno/bin/activate al
activarlo:

(entorno)_usuario:~/APAV$ deactivate
usuario:~/APAV$

10.4.2. Selección de las versiones de los paquetes y congelación del entorno vir-
tual.

El principal objetivo de los entornos virtuales es garantizar que las versiones de todos los paquetes
instalados en él son compatibles con una o varias aplicaciones concretas. Por tanto, es necesario ser
capaz de especificar la versión al instalar un paquete. Esto se consigue indicando la versión deseada a
pip con la sintaxis paquete==version.
Por ejemplo, si estuviéramos interesados en instalar la versión 1.18.15 de numpy (la versión actual es la
1.20.3), lo que haríamos sería:

(entorno)_usuario:~/APAV$ pip install numpy==1.18.15

Podemos ver qué paquetes y qué versiones tenemos instalados con la orden pip list, comprobando
que, efectivamente, se ha instalado la versión deseada:

(entorno)_usuario:~/APAV$ pip list


numpy (1.18.15)

Otra alternativa es el comando pip freeze, que también muestra los paquetes instalados y sus versiones,
pero lo hace en el mismo formato que se usa para instalarlos:

(entorno)_usuario:~/APAV$ pip freeze


numpy==1.18.15

El nombre del comando, freeze (congelar en inglés), viene del hecho de que permite congelar el estado
de un entorno, almacenando en un fichero todos los paquetes instalados en él junto con su respectiva
versión. Lo hacemos redireccionando la salida de pip freeze a un fichero, que habitualmente se llama
[Link]:

197
CAPÍTULO 10. SCRIPTS, MÓDULOS, ENTORNOS VIRTUALES Y PROGRAMAS.

(entorno)_usuario:~/APAV$ pip freeze > [Link]

Más tarde, si queremos instalar exactamente las mismas versiones de estos paquetes en otro entorno
virtual, podemos hacerlo con la opción -r de pip, que justamente toma como argumento un fichero de
texto con los paquetes que debe instalar en el formato proporcionado por pip freeze:

(entorno)_usuario:~/APAV$ "Salimos del entorno virtual"


(entorno)_usuario:~/APAV$ deactivate
usuario:~/APAV$ "Creamos otro entorno virtual"
usuario:~/APAV$ python3.8 -m venv otro_entorno_distinto
usuario:~/APAV$ "Activamos el entorno nuevo"
usuario:~/APAV$ source otro_entorno_distinto/bin/activate
(otro_entorno_distinto)_usuario:~/APAV$ "No tiene hay paquete instalado todavía"
(otro_entorno_distinto)_usuario:~/APAV$ pip freeze
(otro_entorno_distinto)_usuario:~/APAV$ "Instalamos los mismos paquetes que antes"
(otro_entorno_distinto)_usuario:~/APAV$ pip -r [Link]
(otro_entorno_distinto)_usuario:~/APAV$ "Ahora tenemos los mismos paquetes"
(otro_entorno_distinto)_usuario:~/APAV$ pip freeze
numpy==1.18.15

De este modo, es fácil duplicar entornos virtuales. De hecho, se usa habitualmente cuando se distri-
buyen paquetes de software en Python.

[Link]. Otros gestores de entornos virtuales.

Aparte de venv, y con más o menos la misma filosofía, tenemos toda una variedad de gestores de
entornos virtuales: Virtualenv , pyenv , etc. Entre todos ellos, destaca conda , el gestor de paquetes y
entornos virtuales (todo-en-uno) de Anaconda , que está especialmente dirigido a aplicaciones científicas
y de ingeniería de datos, y facilita enormemente la instalación y mantenimiento de los paquetes de
Numpy/Scipy.
Todos ellos permiten realizar tareas semejantes a las realizadas con pip/venv, pero con distintas am-
pliaciones o mejoras. Por ejemplo, facilitar el paso de un entorno a otro, usar lenguajes distintos de
Python, facilitar la distribución de los paquetes y programas incluidos en el entorno, etc.
En general, su uso es muy semejante a venv, pero cada empresa u organización tendrá preferencia por
uno u otro.

198

También podría gustarte