A Prende Python
A Prende Python
A Prende Python
13 de octubre de 2022
Core
1 Introducción 3
1.1 Hablando con la máquina . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3
1.2 Algo de historia . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7
1.3 Python . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13
2 Entornos de desarrollo 21
2.1 Thonny . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 22
2.2 Contexto real . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 26
3 Tipos de datos 35
3.1 Datos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 36
3.2 Números . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 47
3.3 Cadenas de texto . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 62
4 Control de flujo 87
4.1 Condicionales . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 88
4.2 Bucles . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 105
6 Modularidad 185
6.1 Funciones . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 186
6.2 Objetos y Clases . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 226
6.3 Excepciones . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 254
6.4 Módulos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 261
i
7 Procesamiento de texto 269
7.1 string . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 270
9 Scraping 449
9.1 requests . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 450
9.2 beautifulsoup . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 458
9.3 selenium . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 473
ii
Aprende Python
Curso gratuito para aprender el lenguaje de programación Python con un enfoque práctico,
incluyendo ejercicios y cobertura para distintos niveles de conocimiento.1
Licencia: GNU General Public License v3.0: GPLv3.
Consejo: «Programming is not about typing, it’s about thinking.» – Rich Hickey
1
En la foto de portada aparecen los Monty Python. Fuente: noticiascyl
Core 1
Aprende Python
2 Core
CAPÍTULO 1
Introducción
Este capítulo es una introducción a la programación para conocer, desde un enfoque sencillo
pero aclaratorio, los mecanismos que hay detrás de ello.
3
Aprende Python
Los ordenadores son dispositivos complejos pero están diseñados para hacer una cosa bien:
ejecutar aquello que se les indica. La cuestión es cómo indicar a un ordenador lo que
queremos que ejecute. Esas indicaciones se llaman técnicamente instrucciones y se expresan
en un lenguaje. Podríamos decir que programar consiste en escribir instrucciones para que
sean ejecutadas por un ordenador. El lenguaje que utilizamos para ello se denomina lenguaje
de programación.1
Pero aún seguimos con el problema de cómo hacer que un ordenador (o máquina) entienda
el lenguaje de programación. A priori podríamos decir que un ordenador sólo entiende un
lenguaje muy «simple» denominado código máquina. En este lenguaje se utilizan únicamente
los símbolos 0 y 1 en representación de los niveles de tensión alto y bajo, que al fin y al
cabo, son los estados que puede manejar un circuito digital. Hablamos de sistema binario. Si
tuviéramos que escribir programas de ordenador en este formato sería una tarea ardua, pero
afortunadamente se han ido creando con el tiempo lenguajes de programación intermedios
que, posteriormente, son convertidos a código máquina.
Si intentamos visualizar un programa en código máquina, únicamente obtendríamos una
secuencia de ceros y unos:
1.1.2 Ensamblador
SYS_SALIDA equ 1
section .data
msg db "Hello, World",0x0a
len equ $ - msg ;longitud de msg
section .text
global _start ;para el linker
_start: ;marca la entrada
mov eax, 4 ;llamada al sistema (sys_write)
(continué en la próxima página)
1
Foto original por Garett Mizunaka en Unsplash.
4 Capítulo 1. Introducción
Aprende Python
Aunque resulte difícil de creer, lo «único» que hace este programa es mostrar en la pantalla
de nuestro ordenador la frase «Hello, World», pero además teniendo en cuenta que sólo
funcionará para una arquitectura x86.
1.1.3 C
Aunque el lenguaje ensamblador nos facilita un poco la tarea de desarrollar programas, sigue
siendo bastante complicado ya que las instrucciones son muy específicas y no proporcionan
una semántica entendible. Uno de los lenguajes que vino a suplir – en parte – estos obstáculos
fue C. Considerado para muchas personas como un referente en cuanto a los lenguajes de
programación, permite hacer uso de instrucciones más claras y potentes. El mismo ejemplo
anterior del programa «Hello, World» se escribiría así en lenguaje C :
#include <stdio.h>
int main() {
printf("Hello, World");
return 0;
}
1.1.4 Python
¡Pues así de fácil! Hemos pasado de código máquina (ceros y unos) a código Python en el
que se puede entender perfectamente lo que estamos indicando al ordenador. La pregunta
que surge es: ¿cómo entiende una máquina lo que tiene que hacer si le pasamos un programa
hecho en Python (o cualquier otro lenguaje de alto nivel)? La respuesta es un compilador.
1.1.5 Compiladores
Los compiladores son programas que convierten un lenguaje «cualquiera» en código máquina.
Se pueden ver como traductores, permitiendo a la máquina interpretar lo que queremos hacer.
Nota: Para ser más exactos, en Python hablamos de un intérprete en vez de un compilador,
pero a los efectos es prácticamente lo mismo. La diferencia está en que el intérprete realiza
la «compilación» (interpretación) y la «ejecución» de una vez, mientras que el compilador
genera un formato «ejecutable» (código objeto) que se ejecuta en otra fase posterior.
2
Iconos originales por Flaticon.
6 Capítulo 1. Introducción
Aprende Python
1
Foto original por Dario Veronesi en Unsplash.
Luego los avances en las ciencias informáticas han sido muy acelerados, se reemplazaron los
tubos de vacío por transistores en 1958 y en el mismo año, se sustituyeron por circuitos
integrados, y en 1961 se miniaturizaron en chips de silicio. En 1971 apareció el primer
microprocesador de Intel; y en 1973 el primer sistema operativo CP/M. El primer computador
personal es comercializado por IBM en el año 1980.
2
Fuente: Meatze.
8 Capítulo 1. Introducción
Aprende Python
De acuerdo a este breve viaje por la historia, la programación está vinculada a la aparición
de los computadores, y los lenguajes tuvieron también su evolución. Inicialmente, como ya
hemos visto, se programaba en código binario, es decir en cadenas de 0s y 1s, que es el
lenguaje que entiende directamente el computador, tarea extremadamente difícil; luego se
creó el lenguaje ensamblador, que aunque era lo mismo que programar en binario, al estar
en letras era más fácil de recordar. Posteriormente aparecieron lenguajes de alto nivel,
que en general, utilizan palabras en inglés, para dar las órdenes a seguir, para lo cual utilizan
un proceso intermedio entre el lenguaje máquina y el nuevo código llamado código fuente,
este proceso puede ser un compilador o un intérprete.
Un compilador lee todas las instrucciones y genera un resultado; un intérprete ejecuta
y genera resultados línea a línea. En cualquier caso han aparecido nuevos lenguajes de
programación, unos denominados estructurados y en la actualidad en cambio los lenguajes
orientados a objetos y los lenguajes orientados a eventos.3
10 Capítulo 1. Introducción
Aprende Python
12 Capítulo 1. Introducción
Aprende Python
1.3 Python
1.3. Python 13
Aprende Python
Ventajas
Desventajas
14 Capítulo 1. Introducción
Aprende Python
1.3. Python 15
Aprende Python
16 Capítulo 1. Introducción
Aprende Python
1.3. Python 17
Aprende Python
1.3.4 CPython
Nivel avanzado
Existen múltiples implementaciones de Python según el lenguaje de programación que se
ha usado para desarrollarlo. Veamos algunas de ellas:
Implementación Lenguaje
CPython C
Jython Java
IronPython C#
Brython JavaScript
PyPy Python (JIT)
MicroPython C
18 Capítulo 1. Introducción
Aprende Python
Existen una serie de reglas «filosóficas» que indican una manera de hacer y de pensar dentro
del mundo pitónico2 creadas por Tim Peters, llamadas el Zen de Python y que se pueden
aplicar incluso más allá de la programación:
En su traducción de la Wikipedia:
• Bello es mejor que feo.
• Explícito es mejor que implícito.
• Simple es mejor que complejo.
• Complejo es mejor que complicado.
• Plano es mejor que anidado.
• Espaciado es mejor que denso.
• La legibilidad es importante.
2
Dícese de algo/alguien que sigue las convenciones de Python.
1.3. Python 19
Aprende Python
• Los casos especiales no son lo suficientemente especiales como para romper las reglas.
• Sin embargo la practicidad le gana a la pureza.
• Los errores nunca deberían pasar silenciosamente.
• A menos que se silencien explícitamente.
• Frente a la ambigüedad, evitar la tentación de adivinar.
• Debería haber una, y preferiblemente solo una, manera obvia de hacerlo.
• A pesar de que esa manera no sea obvia a menos que seas Holandés.
• Ahora es mejor que nunca.
• A pesar de que nunca es muchas veces mejor que ahora mismo.
• Si la implementación es difícil de explicar, es una mala idea.
• Si la implementación es fácil de explicar, puede que sea una buena idea.
• Los espacios de nombres son una gran idea, ¡tengamos más de esos!
20 Capítulo 1. Introducción
CAPÍTULO 2
Entornos de desarrollo
Para poder utilizar Python debemos preparar nuestra máquina con las herramientas
necesarias. Este capítulo trata sobre la instalación y configuración de los elementos adecuados
para el desarrollo con el lenguaje de programación Python.
21
Aprende Python
2.1 Thonny
Thonny es un programa muy interesante para empezar a aprender Python, ya que engloba
tres de las herramientas fundamentales para trabajar con el lenguaje: intérprete, editor y
depurador.1
Cuando vamos a trabajar con Python debemos tener instalado, como mínimo, un intérprete
del lenguaje (para otros lenguajes sería un compilador). El intérprete nos permitirá
ejecutar nuestro código para obtener los resultados deseados. La idea del intéprete es lanzar
instrucciones «sueltas» para probar determinados aspectos.
Pero normalmente queremos ir un poco más allá y poder escribir programas algo más largos,
por lo que también necesitaremos un editor. Un editor es un programa que nos permite
crear ficheros de código (en nuestro caso con extensión *.py), que luego son ejecutados por
el intérprete.
Hay otra herramienta interesante dentro del entorno de desarrollo que sería el depurador.
Lo podemos encontrar habitualmente en la bibliografía por su nombre inglés debugger.
Es el módulo que nos permite ejecutar paso a paso nuestro código y visualizar qué está
ocurriendo en cada momento. Se suele usar normalmente para encontrar fallos (bugs) en
nuestros programas y poder solucionarlos (debug/fix).
Cuando nos encontramos con un programa que proporciona estas funciones (e incluso otras
adicionales) para el trabajo de programación, nos referimos a él como un Entorno Integrado de
Desarrollo, conocido popularmente por sus siglas en inglés IDE (por Integrated Development
1
Foto original de portada por freddie marriage en Unsplash.
2.1.1 Instalación
Para instalar Thonny debemos acceder a su web y descargar la aplicación para nuestro
sistema operativo. La ventaja es que está disponible tanto para Windows, Mac y Linux.
Una vez descargado el fichero lo ejecutamos y seguimos su instalación paso por paso.
Una vez terminada la instalación ya podemos lanzar la aplicación que se verá parecida a la
siguiente imagen:
Nota: Es posible que el aspecto del programa varíe ligeramente según el sistema operativo,
configuración de escritorio, versión utilizada o idioma (en mi caso está en inglés), pero a
efectos de funcionamiento no hay diferencia.
2.1. Thonny 23
Aprende Python
Para hacer una prueba inicial del intérprete vamos a retomar el primer programa que se
suele hacer. Es el llamado «Hello, World». Para ello escribimos lo siguiente en el intérprete
y pulsamos la tecla ENTER:
Lo que hemos hecho es indicarle a Python que ejecute como entrada la instrucción
print( Hello, World ). La salida es el texto Hello, World que lo vemos en la siguiente
línea (ya sin el prompt >>>).
Importante: Los ficheros que contienen programas hechos en Python siempre deben tener
la extensión .py
2
Término inglés que se refiere al símbolo que precede la línea de comandos.
3
La carpeta donde se guarden los archivos de código no es crítico para su ejecución, pero sí es importante
mantener un orden y una organización para tener localizados nuestros ficheros y proyectos.
Ahora ya podemos ejecutar nuestro fichero helloworld.py. Para ello pulsamos el botón verde
con triángulo blanco (en la barra de herramientas) o bien damos a la tecla F5. Veremos que
en el panel de Shell nos aparece la salida esperada. Lo que está pasando «entre bambalinas»
es que el intérprete de Python está recibiendo como entrada el fichero que hemos creado; lo
ejecuta y devuelve la salida para que Thonny nos lo muestre en el panel correspondiente.
Aunque ya lo veremos en profundidad, lo que hemos hecho es añadir una variable msg en
la línea 1 para luego utilizarla al mostrar por pantalla su contenido. Si ahora volvemos a
ejecutar nuestro programa veremos que en el panel de variables nos aparece la siguiente
información:
Name Value
msg Hello, World
2.1. Thonny 25
Aprende Python
sesión de depuración y podemos avanzar instrucción por instrucción usando la tecla F7:
Hemos visto que Thonny es una herramienta especialmente diseñada para el aprendizaje de
Python, integrando diferentes módulos que facilitan su gestión. Si bien lo podemos utilizar
para un desarrollo más «serio», se suele recurrir a un flujo de trabajo algo diferente en
contextos más reales.1
2.2.1 Python
La forma más habitual de instalar Python (junto con sus librerías) es descargarlo e instalarlo
desde su página oficial:
• Versiones de Python para Windows
• Versiones de Python para Mac
• Versiones de Python para Linux
Anaconda
Otra de las alternativas para disponer de Python en nuestro sistema y que además es muy
utilizada, es Anaconda. Se trata de un conjunto de herramientas, orientadas en principio a
la ciencia de datos, pero que podemos utilizarlas para desarrollo general en Python (junto
con otras librerías adicionales).
Existen versiones de pago, pero la distribución Individual Edition es «open-source» y
gratuita. Se puede descargar desde su página web. Anaconda trae por defecto una gran
cantidad de paquetes Python en su distribución.
Ver también:
Miniconda es un instalador mínimo que trae por defecto Python y un pequeño número de
paquetes útiles.
La instalación limpia2 de Python ya ofrece de por sí muchos paquetes y módulos que vienen
por defecto. Es lo que se llama la librería estándar. Pero una de las características más
destacables de Python es su inmenso «ecosistema» de paquetes disponibles en el Python
Package Index (PyPI).
Para gestionar los paquetes que tenemos en nuestro sistema se utiliza la herramienta pip,
una utilidad que también se incluye en la instalación de Python. Con ella podremos instalar,
1
Foto original de portada por SpaceX en Unsplash.
2
También llamada «vanilla installation» ya que es la que viene por defecto y no se hace ningúna
personalización.
Consejo: Para el caso de Anaconda usaríamos conda install pandas (aunque ya viene
preinstalado).
Nivel intermedio
Cuando trabajamos en distintos proyectos, no todos ellos requieren los mismos paquetes
ni siquiera la misma versión de Python. La gestión de estas situaciones no es sencilla si
únicamente instalamos paquetes y manejamos configuraciones a nivel global (a nivel de
máquina). Es por ello que surge el concepto de entornos virtuales. Como su propio nombre
indica se trata de crear distintos entornos en función de las necesidades de cada proyecto, y
esto nos permite establecer qué versión de Python usaremos y qué paquetes instalaremos.
La manera más sencilla de crear un entorno virtual es la siguiente:
1 $ cd myproject
2 $ python3 -m venv .venv
3 $ source .venv/bin/activate
virtualenv
Si bien con virtualenv tenemos las funcionalidades necesarias para trabajar con entornos
virtuales, destacaría una herramienta llamada virtualenvwrapper que funciona por encima
de virtualenv y que facilita las operaciones sobre entornos virtuales. Su instalación es
equivalente a cualquier otro paquete Python:
$ pip install virtualenvwrapper
pyenv
pyenv permite cambiar fácilmente entre múltiples versiones de Python en un mismo sistema.
Su instalación engloba varios pasos y está bien explicada en la página del proyecto.
La mayor diferencia con respecto a virtualenv es que no instala las distintas versiones de
Python a nivel global del sistema. En vez de eso, se suele crear una carpeta .pyenv en el
HOME del usuario, donde todo está aislado sin generar intrusión en el sistema operativo.
Podemos hacer cosas como:
• Listar las versiones de Python instaladas:
$ pyenv versions
3.7.4
* 3.5.0 (set by /Users/yuu/.pyenv/version)
miniconda3-3.16.0
pypy-2.6.0
$ python --version
Python 3.5.0
$ python --version
Python 3.7.4
$ cd /cool-project
$ pyenv local 3.9.1
$ python --version
Python 3.9.1
2.2.4 Editores
Existen multitud de editores en el mercado que nos pueden servir perfectamente para escribir
código Python. Algunos de ellos incorporan funcionalidades extra y otros simplemente nos
permiten editar ficheros. Cabe destacar aquí el concepto de Entorno de Desarrollo
Integrado, más conocido por sus siglas en inglés IDE3 . Se trata de una aplicación
informática que proporciona servicios integrales para el desarrollo de software.
Podríamos decir que Thonny es un IDE de aprendizaje, pero existen muchos otros. Veamos
un listado de editores de código que se suelen utilizar para desarrollo en Python:
• Editores generales o IDEs con soporte para Python:
– Eclipse + PyDev
– Sublime Text
– Atom
– GNU Emacs
– Vi-Vim
– Visual Studio (+ Python Tools)
– Visual Studio Code (+ Python Tools)
• Editores o IDEs específicos para Python:
3
Integrated Development Environment.
– PyCharm
– Spyder
– Thonny
Cada editor tiene sus características (ventajas e inconvenientes). Supongo que la preferencia
por alguno de ellos estará en base a la experiencia y a las necesidades que surjan. La parte
buena es que hay diversidad de opciones para elegir.
Truco: Visual Studio Code también dispone de integración con Jupyter Notebooks.
2.2.6 repl.it
Figura 4: repl.it
• Almacenamiento de 500MB.
• Python 3.8.2 (febrero de 2022).
• 117 paquetes preinstalados (febrero de 2022).
• Navegador (y subida) de ficheros integrado.
• Gestor de paquetes integrado.
• Integración con GitHub.
• Gestión de secretos (datos sensibles).
• Base de datos clave-valor ya integrada.
• Acceso (limitado) al sistema operativo y sistema de ficheros.
2.2.7 WSL
Linux nativo. Es importante también saber que existen dos versiones de WSL hoy en día:
WSL y WSL2. La segunda es bastante reciente (publicada a mediados de 2019), tiene mejor
rendimiento y se adhiere más al comportamiento de un Linux nativo.
Para la instalación de WSL7 hay que seguir los siguientes pasos:
1. Lanzamos Powershell con permisos de administrador.
2. Activamos la característica de WSL:
4. Finalmente, la instalamos:
$ Add-AppxPackage .\Ubuntu.appx
En este punto, WSL debería estar instalado correctamente, y debería también aparecer en
el menú Inicio.
7
Tutorial de instalación de WSL.
Tipos de datos
Igual que en el mundo real cada objeto pertenece a una categoría, en programación
manejamos objetos que tienen asociado un tipo determinado. En este capítulo se verán
los tipos de datos básicos con los que podemos trabajar en Python.
35
Aprende Python
3.1 Datos
Los programas están formados por código y datos. Pero a nivel interno de la memoria del
ordenador no son más que una secuencia de bits. La interpretación de estos bits depende del
lenguaje de programación, que almacena en la memoria no sólo el puro dato sino distintos
metadatos.1
Cada «trozo» de memoria contiene realmente un objeto, de ahí que se diga que en Python
todo son objetos. Y cada objeto tiene, al menos, los siguientes campos:
• Un tipo del dato almacenado.
• Un identificador único para distinguirlo de otros objetos.
• Un valor consistente con su tipo.
A continuación se muestran los distintos tipos de datos que podemos encontrar en Python,
sin incluir aquellos que proveen paquetes externos:
1
Foto original de portada por Alexander Sinn en Unsplash.
3.1. Datos 37
Aprende Python
3.1.2 Variables
Las variables son fundamentales ya que permiten definir nombres para los valores que
tenemos en memoria y que vamos a usar en nuestro programa.
Podemos obtener un listado de las palabras reservadas del lenguaje de la siguiente forma:
Here is a list of the Python keywords. Enter any keyword to get more help.
Nota: Por lo general se prefiere dar nombres en inglés a las variables que utilicemos, ya que
así hacemos nuestro código más «internacional» y con la posibilidad de que otras personas
puedan leerlo, entenderlo y – llegado el caso – modificarlo. Es sólo una recomendación, nada
impide que se haga en castellano.
Importante: Los nombres de variables son «case-sensitive»3 . Por ejemplo, stuff y Stuff
son nombres diferentes.
3.1. Datos 39
Aprende Python
Mientras se sigan las reglas que hemos visto para nombrar variables no hay problema en la
forma en la que se escriban, pero sí existe una convención para la nomenclatura de las
variables. Se utiliza el llamado snake_case en el que utilizamos caracteres en minúsculas
(incluyendo dígitos si procede) junto con guiones bajos – cuando sean necesarios para su
legibilidad –.4 Por ejemplo, para nombrar una variable que almacene el número de canciones
en nuestro ordenador, podríamos usar num_songs.
Esta convención, y muchas otras, están definidas en un documento denominado PEP 8. Se
trata de una guía de estilo para escribir código en Python. Los PEPs5 son las propuestas
que se hacen para la mejora del lenguaje.
Aunque hay múltiples herramientas disponibles para la comprobación del estilo de código,
una bastante accesible es http://pep8online.com/ ya que no necesita instalación, simplemente
pegar nuestro código y verificar.
Constantes
Un caso especial y que vale la pena destacar son las constantes. Podríamos decir que es un
tipo de variable pero que su valor no cambia a lo largo de nuestro programa. Por ejemplo
la velocidad de la luz. Sabemos que su valor es constante de 300.000 km/s. En el caso
de las constantes utilizamos mayúsculas (incluyendo guiones bajos si es necesario) para
nombrarlas. Para la velocidad de la luz nuestra constante se podría llamar: LIGHT_SPEED.
Se suele decir que una persona programadora (con cierta experiencia), a lo que dedica más
tiempo, es a buscar un buen nombre para sus variables. Quizás pueda resultar algo excesivo
pero da una idea de lo importante que es esta tarea. Es fundamental que los nombres de
variables sean autoexplicativos, pero siempre llegando a un compromiso entre ser concisos
y claros.
Supongamos que queremos buscar un nombre de variable para almacenar el número de
elementos que se deben manejar en un pedido:
1. n
2. num_elements
3. number_of_elements
4. number_of_elements_to_be_handled
4
Más información sobre convenciones de nombres en PEP 8.
5
Del término inglés «Python Enhancement Proposals».
No existe una regla mágica que nos diga cuál es el nombre perfecto, pero podemos aplicar
el sentido común y, a través de la experiencia, ir detectando aquellos nombres que sean más
adecuados. En el ejemplo anterior, quizás podríamos descartar de principio la opción 1 y la
4 (por ser demasiado cortas o demasiado largas); nos quedaríamos con las otras dos. Si nos
fijamos bien, casi no hay mucha información adicional de la opción 3 con respecto a la 2. Así
que podríamos concluir que la opción 2 es válida para nuestras necesidades. En cualquier
caso esto dependerá siempre del contexto del problema que estemos tratando.
Como regla general:
• Usar nombres para variables (ejemplo article).
• Usar verbos para funciones (ejemplo get_article()).
• Usar adjetivos para booleanos (ejemplo available).
3.1.3 Asignación
3.1. Datos 41
Aprende Python
Python nos ofrece la posibilidad de hacer una asignación múltiple de la siguiente manera:
En este caso las tres variables utilizadas en el «lado izquierdo» tomarán el valor 3.
Recordemos que los nombres de variables deben seguir unas reglas establecidas, de lo contrario
obtendremos un error sintáctico del intérprete de Python:
Las asignaciones que hemos hecho hasta ahora han sido de un valor literal a una variable.
Pero nada impide que podamos hacer asignaciones de una variable a otra variable:
Eso sí, la variable que utilicemos como valor de asignación debe existir previamente, ya
que si no es así, obtendremos un error informando de que no está definida:
Hemos visto previamente cómo asignar un valor a una variable, pero aún no sabemos cómo
«comprobar» el valor que tiene dicha variable. Para ello podemos utilizar dos estrategias:
1. Si estamos en una «shell» de Python, basta con que usemos el nombre de la variable:
final_stock = 38934
print(final_stock)
Nota: print sirve también cuando estamos en una sesión interactiva de Python («shell»)
Para poder descubrir el tipo de un literal o una variable, Python nos ofrece la función type().
Veamos algunos ejemplos de su uso:
>>> type(9)
int
>>> type(1.2)
float
Ejercicio
1. Asigna un valor entero 2001 a la variable space_odyssey y muestra su valor.
3.1. Datos 43
Aprende Python
3.1.4 Mutabilidad
Nivel avanzado
Las variables son nombres, no lugares. Detrás de esta frase se esconde la reflexión de que
cuando asignamos un valor a una variable, lo que realmente está ocurriendo es que se hace
apuntar el nombre de la variable a una zona de memoria en el que se representa el objeto
(con su valor).
Si realizamos la asignación de una variable a un valor lo que está ocurriendo es que el nombre
de la variable es una referencia al valor, no el valor en sí mismo:
>>> a = 5
Si ahora «copiamos» el valor de a en otra variable b se podría esperar que hubiera otro
espacio en memoria para dicho valor, pero como ya hemos dicho, son referencias a memoria:
>>> b = a
>>> id(b)
4445989712
¿Y esto qué tiene que ver con la mutabilidad? Pues se dice, por ejemplo, que un entero es
inmutable ya que a la hora de modificar su valor obtenemos una nueva zona de memoria,
o lo que es lo mismo, un nuevo objeto:
>>> a = 5
>>> id(a)
(continué en la próxima página)
6
Esto es un detalle de implementación de CPython.
3.1. Datos 45
Aprende Python
>>> a = 7
>>> id(a)
4310690288
Sin embargo, si tratamos con listas, podemos ver que la modificación de alguno de sus
valores no implica un cambio en la posición de memoria de la variable, por lo que se habla
de objetos mutables.
Ejecución paso a paso a través de Python Tutor:
https://cutt.ly/lvCyXeL
La característica de que los nombres de variables sean referencias a objetos en memoria es
la que hace posible diferenciar entre objetos mutables e inmutables:
Inmutable Mutable
bool list
int set
float dict
str
tuple
Importante: El hecho de que un tipo de datos sea inmutable significa que no podemos
modificar su valor «in-situ», pero siempre podremos asignarle un nuevo valor (hacerlo apuntar
a otra zona de memoria).
Nivel intermedio
Hemos ido usando una serie de funciones sin ser especialmente conscientes de ello. Esto se
debe a que son funciones «built-in» o incorporadas por defecto en el propio lenguaje Python.
AMPLIAR CONOCIMIENTOS
3.2 Números
3.2. Números 47
Aprende Python
En esta sección veremos los tipos de datos númericos que ofrece Python centrándonos en
booleanos, enteros y flotantes.1
3.2.1 Booleanos
George Boole es considerado como uno de los fundadores del campo de las ciencias de la
computación y fue el creador del Álgebra de Boole que da lugar, entre otras estructuras
algebraicas, a la Lógica binaria. En esta lógica las variables sólo pueden tomar dos valores
discretos: verdadero o falso.
El tipo de datos bool proviene de lo explicado anteriormente y admite dos posibles valores:
• True que se corresponde con verdadero (y también con 1 en su representación
numérica).
• False que se corresponde con falso (y también con 0 en su representación numérica).
Veamos un ejemplo de su uso:
La primera variable is_opened está representando el hecho de que algo esté abierto, y al
tomar el valor True podemos concluir que sí. La segunda variable has_sugar nos indica si
una bebida tiene azúcar; dado que toma el valor False inferimos que no lleva azúcar.
Atención: Tal y como se explicó en este apartado, los nombres de variables son
«case-sensitive». De igual modo el tipo booleano toma valores True y False con la
primera letra en mayúsculas. De no ser así obtendríamos un error sintáctico.
1
Foto original de portada por Brett Jordan en Unsplash.
3.2.2 Enteros
Los números enteros no tienen decimales pero sí pueden contener signo y estar expresados
en alguna base distinta de la usual (base 10).
Literales enteros
>>> 8
8
>>> 0
0
>>> 08
File "<stdin>", line 1
08
^
SyntaxError: invalid token
>>> 99
99
>>> +99
99
>>> -99
-99
>>> 3000000
3000000
>>> 3_000_000
3000000
A continuación se muestra una tabla con las distintas operaciones sobre enteros que podemos
realizar en Python:
3.2. Números 49
Aprende Python
>>> 2 + 8 + 4
14
>>> 4 ** 4
256
>>> 7 / 3
2.3333333333333335
>>> 7 // 3
2
>>> 6 / 0
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
ZeroDivisionError: division by zero
Es de buen estilo de programación dejar un espacio entre cada operador. Además hay que
tener en cuenta que podemos obtener errores dependiendo de la operación (más bien de los
operandos) que estemos utilizando, como es el caso de la división por cero.
Igualmente es importante tener en cuenta la prioridad de los distintos operadores:
Prioridad Operador
1 (mayor) ()
2 **
3 -a +a
4 * / // %
5 (menor) +-
>>> 2 ** 2 + 4 / 2
6.0
>>> 2 ** (2 + 4) / 2
32.0
Asignación aumentada
Supongamos que disponemos de 100 vehículos en stock y que durante el pasado mes se han
vendido 20 de ellos. Veamos cómo sería el código con asignación tradicional vs. asignación
aumentada:
Estas dos formas son equivalentes a nivel de resultados y funcionalidad, pero obviamente
tienen diferencias de escritura y legibilidad. De este mismo modo, podemos aplicar un formato
compacto al resto de operaciones:
3.2. Números 51
Aprende Python
>>> random_number = 15
>>> random_number += 5
>>> random_number
20
>>> random_number *= 3
>>> random_number
60
Módulo
La operación módulo (también llamado resto), cuyo símbolo en Python es %, se define como
el resto de dividir dos números. Veamos un ejemplo para enteder bien su funcionamiento:
>>> dividendo = 17
>>> divisor = 5
>>> cociente
3
>>> resto
2
Exponenciación
>>> 4 ** 3
64
>>> 4 * 4 * 4
64
Se debe tener en cuenta que también podemos elevar un número entero a un número
decimal. En este caso es como si estuviéramos haciendo una raíz 2 . Por ejemplo:
1 √
4 2 = 40.5 = 4=2
Hecho en Python:
>>> 4 ** 0.5
2.0
Ejercicio
Una ecuación de segundo grado se define como 𝑎𝑥2 + 𝑏𝑥 + 𝑐 = 0, y (en determinados casos)
tiene dos soluciones:
√
−𝑏 + 𝑏2 − 4𝑎𝑐
𝑥1 =
√2𝑎
−𝑏 − 𝑏2 − 4𝑎𝑐
𝑥2 =
2𝑎
Dados los coeficientes a=4, b=-6 y c=2 calcule sus dos soluciones. Tenga en cuenta subdividir
los cálculos y reutilizar variables (por ejemplo el discriminante).
La solución para los valores anteriores es:
• x1 = 1
• x2 = 0.5
Recuerde que la raíz cuadrada se puede calcular como la exponenciación a 12 .
Puede comprobar los resultados para otros valores de entrada con esta aplicación para
resolver ecuaciones cuadráticas.
2
No siempre es una raíz cuadrada porque se invierten numerador y denominador.
3.2. Números 53
Aprende Python
Valor absoluto
>>> abs(-1)
1
>>> abs(1)
1
>>> abs(-3.14)
3.14
>>> abs(3.14)
3.14
Límite de un entero
Nivel avanzado
¿Cómo de grande puede ser un int en Python? La respuesta es de cualquier tamaño. Por
poner un ejemplo, supongamos que queremos representar un centillón. Este valor viene a ser
un «1» seguido por ¡600 ceros! ¿Será capaz Python de almacenarlo?
>>> centillion
1000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
Nota: En muchos lenguajes tratar con enteros tan largos causaría un «integer overflow».
No es el caso de Python que puede manejar estos valores sin problema.
3.2.3 Flotantes
Los números en punto flotante3 tienen parte decimal. Veamos algunos ejemplos de
flotantes en Python.
3
Punto o coma flotante es una notación científica usada por computadores.
Conversión de tipos
Conversión implícita
>>> True + 25
26
>>> 7 * False
0
>>> True + False
1
>>> 21.8 + True
22.8
>>> 10 + 11.3
21.3
3.2. Números 55
Aprende Python
Se puede ver claramente que la conversión numérica de los valores booleanos es:
• True 1
• False 0
Conversión explícita
Aunque más adelante veremos el concepto de función, desde ahora podemos decir que
existen una serie de funciones para realizar conversiones explícitas de un tipo a otro:
bool() Convierte el tipo a booleano.
int() Convierte el tipo a entero.
float() Convierte el tipo a flotante.
Veamos algunos ejemplos de estas funciones:
>>> bool(1)
True
>>> bool(0)
False
>>> int(True)
1
>>> int(False)
0
>>> float(1)
1.0
>>> float(0)
0.0
>>> float(True)
1.0
>>> float(False)
0.0
En el caso de que usemos la función int() sobre un valor flotante, nos retorna su parte
baja:
⌊︀ ⌋︀
𝑖𝑛𝑡(𝑥) = 𝑥
Por ejemplo:
>>> int(3.1)
3
>>> int(3.5)
3
>>> int(3.9)
3
Para obtener el tipo de una variable podemos hacer uso de la función type():
>>> is_raining = False
>>> type(is_raining)
bool
>>> sound_level = 35
>>> type(sound_level)
int
Igualmente existe la posibilidad de comprobar el tipo que tiene una variable mediante la
función isinstance():
>>> isinstance(is_raining, bool)
True
>>> isinstance(sound_level, int)
True
>>> isinstance(temperature, float)
True
Ejercicio
Existe una aproximación al seno de un ángulo 𝑥 expresado en grados:
4𝑥(180 − 𝑥)
𝑠𝑖𝑛(𝑥) ≈
40500 − 𝑥(180 − 𝑥)
Calcule dicha aproximación utilizando operaciones en Python. Descomponga la expresión en
subcálculos almacenados en variables. Tenga en cuenta aquellas expresiones comunes para
no repetir cálculos y seguir el principio DRY.
¿Qué tal funciona la aproximación? Compare sus resultados con estos:
• 𝑠𝑖𝑛(90) = 1.0
• 𝑠𝑖𝑛(45) = 0.7071067811865475
• 𝑠𝑖𝑛(50) = 0.766044443118978
3.2. Números 57
Aprende Python
Errores de aproximación
Nivel intermedio
Supongamos el siguiente cálculo:
Debería dar 1.0, pero no es así puesto que la representación interna de los valores en coma
flotante sigue el estándar IEEE 754 y estamos trabajando con aritmética finita.
Aunque existen distintas formas de solventar esta limitación, de momento veremos una de las
más sencillas utilizando la función «built-in» round() que nos permite redondear un número
flotante a un número determinado de decimales:
>>> pi = 3.14159265359
>>> round(pi)
3
>>> round(pi, 1)
3.1
>>> round(pi, 2)
3.14
>>> round(pi, 3)
3.142
>>> round(pi, 4)
3.1416
>>> round(pi, 5)
3.14159
>>> round(result, 1)
1.0
Prudencia: round() aproxima al valor más cercano, mientras que int() obtiene siepre
el entero «por abajo».
Límite de un flotante
A diferencia de los enteros, los números flotantes sí que tienen un límite en Python. Para
descubrirlo podemos ejecutar el siguiente código:
>>> sys.float_info.min
2.2250738585072014e-308
>>> sys.float_info.max
1.7976931348623157e+308
3.2.4 Bases
Nivel intermedio
Los valores numéricos con los que estamos acostumbrados a trabajar están en base 10 (o
decimal). Esto indica que disponemos de 10 «símbolos» para representar las cantidades. En
este caso del 0 al 9.
Pero también es posible representar números en otras bases. Python nos ofrece una serie
de prefijos y funciones para este cometido.
Base binaria
>>> 0b1001
9
>>> 0b1100
12
Función: bin()
>>> bin(9)
0b1001
>>> bin(12)
0b1100
3.2. Números 59
Aprende Python
Base octal
>>> 0o6243
3235
>>> 0o1257
687
Función: oct()
>>> oct(3235)
0o6243
>>> oct(687)
0o1257
Base hexadecimal
>>> 0x7F2A
32554
>>> 0x48FF
18687
Función: hex()
>>> hex(32554)
0x7f2a
>>> hex(18687)
0x48ff
EJERCICIOS DE REPASO
AMPLIAR CONOCIMIENTOS
3.2. Números 61
Aprende Python
Las cadenas de texto son secuencias de caracteres. También se les conoce como «strings»
y nos permiten almacenar información textual de forma muy cómoda.1
Es importante destacar que Python 3 almacena los caracteres codificados en el estándar
Unicode, lo que es una gran ventaja con respecto a versiones antiguas del lenguaje. Además
permite representar una cantidad ingente de símbolos incluyendo los famosos emojis .
Para escribir una cadena de texto en Python basta con rodear los caracteres con comillas
simples6 :
Para incluir comillas dobles dentro de la cadena de texto no hay mayor inconveniente:
1
Foto original de portada por Roman Kraft en Unsplash.
6
También es posible utilizar comillas dobles. Yo me he decantado por las comillas simples ya que quedan
más limpias y suele ser el formato que devuelve el propio intérprete de Python.
Puede surgir la duda de cómo incluimos comillas simples dentro de la propia cadena de
texto. Veamos soluciones para ello:
En la primera opción estamos escapando las comillas simples para que no sean tratadas
como caracteres especiales. En la segunda opción estamos creando el «string» con comillas
dobles (por fuera) para poder incluir directamente las comillas simples (por dentro). Python
también nos ofrece esta posibilidad.
Comillas triples
Hay una forma alternativa de crear cadenas de texto utilizando comillas triples. Su uso está
pensado principalmente para cadenas multilínea:
Importante: Los tres puntos ... que aparecen a la izquierda de las líneas no están incluidos
en la cadena de texto. Es el símbolo que ofrece el intérprete de Python cuando saltamos de
línea.
Cadena vacía
La cadena vacía es aquella que no contiene ningún carácter. Aunque a priori no lo pueda
parecer, es un recurso importante en cualquier código. Su representación en Python es la
siguiente:
>>>
3.3.2 Conversión
Podemos crear «strings» a partir de otros tipos de datos usando la función str():
>>> str(True)
True
>>> str(10)
10
>>> str(21.7)
21.7
>>> int( 10 )
10
>>> float( 21.7 )
21.7
Pero hay que tener en cuenta un detalle. La función int() también admite la base en la
que se encuentra el número. Eso significa que podemos pasar un número, por ejemplo, en
hexadecimal (como «string») y lo podríamos convertir a su valor entero:
Nota: La base por defecto que utiliza int() para convertir cadenas de texto es la base
decimal.
Python permite escapar el significado de algunos caracteres para conseguir otros resultados.
Si escribimos una barra invertida \ antes del carácter en cuestión, le otorgamos un significado
especial.
Quizás la secuencia de escape más conocida es \n que representa un salto de línea, pero
existen muchas otras:
# Salto de línea
>>> msg = Primera línea\nSegunda línea\nTercera línea
>>> print(msg)
Primera línea
Segunda línea
(continué en la próxima página)
# Tabulador
>>> msg = Valor = \t40
>>> print(msg)
Valor = 40
# Comilla simple
>>> msg = Necesitamos \ escapar\ la comilla simple
>>> print(msg)
Necesitamos escapar la comilla simple
# Barra invertida
>>> msg = Capítulo \\ Sección \\ Encabezado
>>> print(msg)
Capítulo \ Sección \ Encabezado
Nota: Al utilizar la función print() es cuando vemos realmente el resultado de utilizar los
caracteres escapados.
Expresiones literales
Nivel intermedio
Hay situaciones en las que nos interesa que los caracteres especiales pierdan ese significado y
poder usarlos de otra manera. Existe un modificar de cadena que proporciona Python para
tratar el texto en bruto. Es el llamado «raw data» y se aplica anteponiendo una r a la cadena
de texto.
Veamos algunos ejemplos:
>>> text = abc\ndef
>>> print(text)
abc
def
Hemos estado utilizando la función print() de forma sencilla, pero admite algunos
parámetros interesantes:
Línea 4: Podemos imprimir todas las variables que queramos separándolas por comas.
Línea 7: El separador por defecto entre las variables es un espacio, podemos cambiar el
carácter que se utiliza como separador entre cadenas.
Línea 10: El carácter de final de texto es un salto de línea, podemos cambiar el carácter
que se utiliza como final de texto.
Los programas se hacen para tener interacción con el usuario. Una de las formas de
interacción es solicitar la entrada de datos por teclado. Como muchos otros lenguajes de
programación, Python también nos ofrece la posibilidad de leer la información introducida
por teclado. Para ello se utiliza la función input():
Nota: La función input() siempre nos devuelve un objeto de tipo cadena de texto o str.
Tenerlo muy en cuenta a la hora de trabajar con números, ya que debemos realizar una
conversión explícita.
Ejercicio
Escriba un programa en Python que lea por teclado dos números enteros y muestre por
pantalla el resultado de realizar las operaciones básicas entre ellos.
Ejemplo
• Valores de entrada 7 y 4.
• Salida esperada:
7+4=11
7-4=3
7*4=28
7/4=1.75
Consejo: Aproveche todo el potencial que ofrece print() para conseguir la salida esperada.
Combinar cadenas
Repetir cadenas
>>> reaction * 4
WowWowWowWow
Obtener un carácter
Los «strings» están indexados y cada carácter tiene su propia posición. Para obtener un
único carácter dentro de una cadena de texto es necesario especificar su índice dentro de
corchetes [...].
>>> sentence[0]
H
>>> sentence[-1]
o
>>> sentence[4]
,
>>> sentence[-5]
M
Truco: Nótese que existen tanto índices positivos como índices negativos para acceder
a cada carácter de la cadena de texto. A priori puede parecer redundante, pero es muy útil
en determinados casos.
En caso de que intentemos acceder a un índice que no existe, obtendremos un error por fuera
de rango:
>>> sentence[50]
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
IndexError: string index out of range
Las cadenas de texto son tipos de datos inmutables. Es por ello que no podemos modificar
un carácter directamente:
>>> song[4] = D
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: str object does not support item assignment
Truco: Existen formas de modificar una cadena de texto que veremos más adelante,
aunque realmente no estemos transformando el original sino creando un nuevo objeto con
las modificaciones.
>>> proverb[:]
Agua pasada no mueve molino
>>> proverb[12:]
no mueve molino
>>> proverb[:11]
Agua pasada
>>> proverb[5:11]
pasada
>>> proverb[5:11:2]
psd
Importante: El troceado siempre llega a una unidad menos del índice final que hayamos
especificado. Sin embargo el comienzo sí coincide con el que hemos puesto.
2
El término usado en inglés es slice.
Para obtener la longitud de una cadena podemos hacer uso de len(), una función común a
prácticamente todos los tipos y estructuras de datos en Python:
>>> empty =
>>> len(empty)
0
Pertenencia de un elemento
Si queremos comprobar que una determinada subcadena se encuentra en una cadena de texto
utilizamos el operador in para ello. Se trata de una expresión que tiene como resultado un
valor «booleano» verdadero o falso:
>>> proverb = Más vale malo conocido que bueno por conocer
Habría que prestar atención al caso en el que intentamos descubrir si una subcadena no está
en la cadena de texto:
Una tarea muy común al trabajar con cadenas de texto es dividirlas por algún tipo
de separador. En este sentido, Python nos ofrece la función split(), que debemos usar
anteponiendo el «string» que queramos dividir:
Aunque aún no lo hemos visto, lo que devuelve split() es una lista (otro tipo de datos en
Python) donde cada elemento es una parte de la cadena de texto original:
>>> type(game.split( - ))
list
Ejercicio
Sabiendo que la longitud de una lista se calcula igual que la longitud de una cadena de texto,
obtenga el número de palabras que contiene la siguiente cadena de texto:
Existe una forma algo más avanzada de dividir una cadena a través del particionado. Para
ello podemos valernos de la función partition() que proporciona Python.
Esta función toma un argumento como separador, y divide la cadena de texto en 3 partes: lo
que queda a la izquiera del separador, el separador en sí mismo y lo que queda a la derecha
del separador:
>>> text = 3 + 4
>>> text.partition( + )
( 3 , + , 4 )
Limpiar cadenas
Cuando leemos datos del usuario o de cualquier fuente externa de información, es bastante
probable que se incluyan en esas cadenas de texto, caracteres de relleno3 al comienzo y
al final. Python nos ofrece la posibilidad de eliminar estos caracteres u otros que no nos
interesen.
La función strip() se utiliza para eliminar caracteres del principio y del final de un
«string». También existen variantes de esta función para aplicarla únicamente al comienzo
o únicamente al final de la cadena de texto.
Supongamos que debemos procesar un fichero con números de serie de un determinado
artículo. Cada línea contiene el valor que nos interesa pero se han «colado» ciertos caracteres
de relleno que debemos limpiar:
>>> serial_number.strip()
48374983274832
Nota: Si no se especifican los caracteres a eliminar, strip() usa por defecto cualquier
combinación de espacios en blanco, saltos de línea \n y tabuladores \t.
A continuación vamos a hacer «limpieza» por la izquierda (comienzo) y por la derecha (final)
utilizando la función lstrip() y rstrip() respectivamente:
Como habíamos comentado, también existe la posibilidad de especificar los caracteres que
queremos borrar:
>>> serial_number.strip( \n )
\t \n 48374983274832 \n\n\t \t
Importante: La función strip() no modifica la cadena que estamos usando (algo obvio
3
Se suele utilizar el término inglés «padding» para referirse a estos caracteres.
porque los «strings» son inmutables) sino que devuelve una nueva cadena de texto con las
modificaciones pertinentes.
Realizar búsquedas
Aunque hemos visto que la forma pitónica de saber si una subcadena se encuentra dentro
de otra es a través del operador in, Python nos ofrece distintas alternativas para realizar
búsquedas en cadenas de texto.
Vamos a partir de una variable que contiene un trozo de la canción Mediterráneo de Joan
Manuel Serrat para ejemplificar las distintas opciones que tenemos:
>>> lyrics.count( mi )
2
>>> lyrics.count( tu )
3
>>> lyrics.count( él )
0
Ejercicio
Dada la siguiente letra5 , obtenga la misma pero sustituyendo la palabra voices por sounds:
Reemplazar elementos
5
«Tenerife Sea» por Ed Sheeran.
Mayúsculas y minúsculas
Python nos permite realizar variaciones en los caracteres de una cadena de texto para pasarlos
a mayúsculas y/o minúsculas. Veamos las distintas opciones disponibles:
>>> proverb
quien a buen árbol se arrima Buena Sombra le cobija
>>> proverb.capitalize()
Quien a buen árbol se arrima buena sombra le cobija
>>> proverb.title()
Quien A Buen Árbol Se Arrima Buena Sombra Le Cobija
>>> proverb.upper()
QUIEN A BUEN ÁRBOL SE ARRIMA BUENA SOMBRA LE COBIJA
>>> proverb.lower()
quien a buen árbol se arrima buena sombra le cobija
>>> proverb.swapcase()
QUIEN A BUEN ÁRBOL SE ARRIMA bUENA sOMBRA LE COBIJA
Identificando caracteres
Hay veces que recibimos información textual de distintas fuentes de las que necesitamos
identificar qué tipo de caracteres contienen. Para ello Python nos ofrece un grupo de
funciones.
Veamos algunas de estas funciones:
En este apartado veremos cómo interpolar valores dentro de cadenas de texto utilizando
diferentes formatos. Interpolar (en este contexto) significa sustituir una variable por su valor
dentro de una cadena de texto.
Veamos los estilos que proporciona Python para este cometido:
Aunque aún podemos encontrar código con el estilo antiguo y el estilo nuevo en el formateo
de cadenas, vamos a centrarnos en el análisis de los «f-strings» que se están utilizando
bastante en la actualidad.
«f-strings»
Los f-strings aparecieron en Python 3.6 y se suelen usar en código de nueva creación. Es
la forma más potente – y en muchas ocasiones más eficiente – de formar cadenas de texto
incluyendo valores de otras variables.
La interpolación en cadenas de texto es un concepto que existe en la gran mayoría de
lenguajes de programación y hace referencia al hecho de sustituir los nombres de variables
por sus valores cuando se construye un «string».
Para indicar en Python que una cadena es un «f-string» basta con precederla de una f e
incluir las variables o expresiones a interpolar entre llaves {...}.
Supongamos que disponemos de los datos de una persona y queremos formar una frase de
bienvenida con ellos:
>>> f Me llamo {name}, tengo {age} años y una fortuna de {fortune} millones
Me llamo Elon Musk, tengo 49 años y una fortuna de 43300 millones
Podría surgir la duda de cómo incluir llaves dentro de la cadena de texto, teniendo en cuenta
que las llaves son símbolos especiales para la interpolación de variables. La respuesta es
duplicar las llaves:
>>> x = 10
Formateando cadenas
Nivel intermedio
Los «f-strings» proporcionan una gran variedad de opciones de formateado: ancho del
texto, número de decimales, tamaño de la cifra, alineación, etc. Muchas de estas facilidades
se pueden consultar en el artículo Best of Python3.6 f-strings4
Dando formato a valores enteros:
4
Escrito por Nirant Kasliwal en Medium.
>>> f {mount_height:10d}
3718
>>> f {mount_height:010d}
0000003718
>>> f {value}
147
>>> f {value:b}
10010011
>>> pi = 3.14159265
>>> f {pi:f} # 6 decimales por defecto (se rellenan con ceros si procede)
3.141593
>>> f {pi:.3f}
3.142
>>> f {pi:12f}
3.141593
>>> f {pi:7.2f}
3.14
>>> f {pi:07.2f}
(continué en la próxima página)
>>> f {pi:.010f}
3.1415926500
>>> f {pi:e}
3.141593e+00
Alineando valores:
>>> f {text1:<7s}|{text2:^11s}|{text3:>7s}
how | are | you
>>> f {text1:-<7s}|{text2:·^11s}|{text3:->7s}
how----|····are····|----you
Modo «debug»
A partir de Python 3.8, los «f-strings» permiten imprimir el nombre de la variable y su valor,
como un atajo para depurar nuestro código. Para ello sólo tenemos que incluir un símbolo =
después del nombre de la variable:
>>> f {serie=}
"serie= The Simpsons "
>>> f {imdb_rating=}
imdb_rating=8.7
Ejercicio
Dada la variable:
e = 2.71828
2.718
2.718280
2.72 # 4 espacios en blanco
2.718280e+00
00002.7183
2.71828 # 12 espacios en blanco
Python trabaja por defecto con caracteres Unicode. Eso significa que tenemos acceso a la
amplia carta de caracteres que nos ofrece este estándar de codificación.
Supongamos un ejemplo sobre el típico «emoji» de un cohete definido en este cuadro:
>>> \N{ROCKET}
Nivel avanzado
Hemos estado usando muchas funciones de objetos tipo «string» (y de otros tipos
previamente). Pero quizás no sabemos aún como podemos descubrir todo lo que podemos
hacer con ellos y los casos de uso que nos ofrece.
Python proporciona una función «built-in» llamada dir() para inspeccionar un determinado
tipo de objeto:
>>> dir(text)
[ __add__ ,
__class__ ,
__contains__ ,
__delattr__ ,
__dir__ ,
__doc__ ,
__eq__ ,
__format__ ,
__ge__ ,
__getattribute__ ,
__getitem__ ,
__getnewargs__ ,
__gt__ ,
__hash__ ,
__init__ ,
__init_subclass__ ,
__iter__ ,
__le__ ,
__len__ ,
__lt__ ,
__mod__ ,
__mul__ ,
(continué en la próxima página)
Esto es aplicable tanto a variables como a literales e incluso a tipos de datos (clases)
explícitos:
>>> dir(10)
[ __abs__ ,
__add__ ,
__and__ ,
__bool__ ,
...
imag ,
numerator ,
real ,
to_bytes ]
>>> dir(float)
[ __abs__ ,
__add__ ,
__bool__ ,
__class__ ,
...
hex ,
imag ,
is_integer ,
real ]
EJERCICIOS DE REPASO
1. Escriba un programa en Python que acepte el nombre y los apellidos de una persona
y los imprima en orden inverso separados por una coma. Utilice f-strings para
implementarlo (solución).
Entrada: nombre=Sergio; apellidos=Delgado Quintero
Salida: Delgado Quintero, Sergio
2. Escriba un programa en Python que acepte una ruta remota de recurso samba, y lo
separe en nombre(IP) del equipo y ruta (solución).
Entrada: //1.1.1.1/eoi/python
Salida: equipo=1.1.1.1; ruta=/eoi/python
3. Escriba un programa en Python que acepte un «string» con los 8 dígitos de un NIF, y
calcule su dígito de control (solución).
Entrada: 12345678
Salida: 12345678Z
4. Escriba un programa en Python que acepte un entero n y compute el valor de n + nn
+ nnn (solución).
Entrada: 5
Salida: 615 (5 + 55 + 555)
5. Escriba un programa en Python que acepte una palabra en castellano y calcule una
métrica que sea el número total de caracteres de la palabra multiplicado por el número
total de vocales que contiene la palabra (solución).
Entrada: ordenador
Salida: 36
AMPLIAR CONOCIMIENTOS
Control de flujo
Todo programa informático está formado por instrucciones que se ejecutan en forma
secuencial de «arriba» a «abajo», de igual manera que leeríamos un libro. Este orden
constituye el llamado flujo del programa. Es posible modificar este flujo secuencial para
que tome bifurcaciones o repita ciertas instrucciones. Las sentencias que nos permiten hacer
estas modificaciones se engloban en el control de flujo.
87
Aprende Python
4.1 Condicionales
En esta sección veremos las sentencias if y match-case junto a las distintas variantes que
pueden asumir, pero antes de eso introduciremos algunas cuestiones generales de escritura
de código.1
A diferencia de otros lenguajes que utilizan llaves para definir los bloques de código,
cuando Guido Van Rossum creó el lenguaje quiso evitar estos caracteres por considerarlos
innecesarios. Es por ello que en Python los bloques de código se definen a través de
espacios en blanco, preferiblemente 4.2 En términos técnicos se habla del tamaño
de indentación.
Consejo: Esto puede resultar extraño e incómodo a personas que vienen de otros lenguajes
de programación pero desaparece rápido y se siente natural a medida que se escribe código.
1
Foto original de portada por ali nafezarefi en Unsplash.
2
Reglas de indentación definidas en PEP 8
4.1.2 Comentarios
Los comentarios son anotaciones que podemos incluir en nuestro programa y que nos
permiten aclarar ciertos aspectos del código. Estas indicaciones son ignoradas por el
intérprete de Python.
Los comentarios se incluyen usando el símbolo almohadilla # y comprenden hasta el final de
la línea.
Los comentarios también pueden aparecer en la misma línea de código, aunque la guía de
estilo de Python no aconseja usarlos en demasía:
4.1. Condicionales 89
Aprende Python
Los programas suelen ser más legibles cuando las líneas no son excesivamente largas. La
longitud máxima de línea recomendada por la guía de estilo de Python es de 80 caracteres.
Sin embargo, esto genera una cierta polémica hoy en día, ya que los tamaños de pantalla
han aumentado y las resoluciones son mucho mayores que hace años. Así las líneas de más
de 80 caracteres se siguen visualizando correctamente. Hay personas que son más estrictas
en este límite y otras más flexibles.
En caso de que queramos romper una línea de código demasiado larga, tenemos dos
opciones:
1. Usar la barra invertida \:
>>> factorial = 4 * 3 * 2 * 1
>>> factorial = 4 * \
... 3 * \
... 2 * \
... 1
>>> factorial = 4 * 3 * 2 * 1
>>> factorial = (4 *
... 3 *
... 2 *
... 1)
4.1.4 La sentencia if
La sentencia condicional en Python (al igual que en muchos otros lenguajes de programación)
es if. En su escritura debemos añadir una expresión de comparación terminando con
dos puntos al final de la línea. Veamos un ejemplo:
>>> temperature = 40
En el caso anterior se puede ver claramente que la condición se cumple y por tanto se ejecuta
la instrucción que tenemos dentro del cuerpo de la condición. Pero podría no ser así. Para
controlar ese caso existe la sentencia else. Veamos el mismo ejemplo anterior pero añadiendo
esta variante:
>>> temperature = 20
>>> temperature = 28
Python nos ofrece una mejora en la escritura de condiciones anidadas cuando aparecen
consecutivamente un else y un if. Podemos sustituirlos por la sentencia elif:
3
El anidamiento (o «nesting») hace referencia a incorporar sentencias unas dentro de otras mediante la
inclusión de diversos niveles de profunidad (indentación).
4.1. Condicionales 91
Aprende Python
>>> temperature = 28
Cuando escribimos condiciones debemos incluir alguna expresión de comparación. Para usar
estas expresiones es fundamental conocer los operadores que nos ofrece Python:
Operador Símbolo
Igualdad ==
Desigualdad !=
Menor que <
Menor o igual que <=
Mayor que >
Mayor o igual que >=
A continuación vamos a ver una serie de ejemplos con expresiones de comparación. Téngase
en cuenta que estas expresiones habría que incluirlas dentro de la sentencia condicional en
el caso de que quisiéramos tomar una acción concreta:
>>> value == 8
True
>>> value != 8
False
4.1. Condicionales 93
Aprende Python
Python ofrece la posibilidad de ver si un valor está entre dos límites de manera directa. Así,
por ejemplo, para descubrir si value está entre 4 y 12 haríamos:
Nota:
1. Una expresión de comparación siempre devuelve un valor booleano, es decir True o
False.
2. El uso de paréntesis, en función del caso, puede aclarar la expresión de comparación.
Ejercicio
Dada una variable year con un valor entero, compruebe si dicho año es bisiesto o no lo es.
Un año es bisiesto en el calendario Gregoriano, si es divisible entre 4 y no divisible entre
100, o bien si es divisible entre 400. Puedes hacer la comprobación en esta lista de años
bisiestos.
Ejemplo
• Entrada: 2008
• Salida: Es un año bisiesto
«Booleanos» en condiciones
>>> if is_cold:
... print( Coge chaqueta )
... else:
... print( Usa camiseta )
...
Coge chaqueta
Hemos visto una comparación para un valor «booleano» verdadero (True). En el caso de que
la comparación fuera para un valor falso lo haríamos así:
Ejercicio
Escriba un programa que permita adivinar un personaje de Marvel en base a las tres
preguntas siguientes:
1. ¿Puede volar?
2. ¿Es humano?
4.1. Condicionales 95
Aprende Python
3. ¿Tiene máscara?
Ejemplo
• Entrada: can_fly = True, is_human = True y has_mask = True
• Salida: Ironman
Es una especie de Akinator para personajes de Marvel…
Valor nulo
Nivel intermedio
None es un valor especial de Python que almacena el valor nulo4 . Veamos cómo se comporta
al incorporarlo en condiciones de veracidad:
>>> if value:
... print( Value has some useful value )
... else:
... # value podría contener None, False (u otro)
... print( Value seems to be void )
...
Value seems to be void
Para distinguir None de los valores propiamente booleanos, se recomienda el uso del operador
is. Veamos un ejemplo en el que tratamos de averiguar si un valor es nulo:
De igual forma, podemos usar esta construcción para el caso contrario. La forma «pitónica»
de preguntar si algo no es nulo es la siguiente:
>>> value = 99
4
Lo que en otros lenguajes se conoce como nil, null, nothing.
4.1. Condicionales 97
Aprende Python
Una de las novedades más esperadas (y quizás controvertidas) de Python 3.10 fue el
llamado Structural Pattern Matching que introdujo en el lenguaje una nueva sentencia
condicional. Ésta se podría asemejar a la sentencia «switch» que ya existe en otros lenguajes
de programación.
Comparando valores
En su versión más simple, el «pattern matching» permite comparar un valor de entrada con
una serie de literales. Algo así como un conjunto de sentencias «if» encadenadas. Veamos
esta aproximación mediante un ejemplo:
¿Qué ocurre si el valor que comparamos no existe entre las opciones disponibles? Pues en
principio, nada, ya que este caso no está cubierto. Si lo queremos controlar, hay que añadir
una nueva regla utilizando el subguión _ como patrón:
Ejercicio
Escriba un programa en Python que pida (por separado) dos valores numéricos y un operando
(suma, resta, multiplicación, división) y calcule el resultado de la operación, usando para ello
la sentencia match-case.
Controlar que la operación no sea una de las cuatro predefinidas. En este caso dar un mensaje
de error y no mostrar resultado final.
Ejemplo
• Entrada: 4, 3, +
• Salida: 4+3=7
Patrones avanzados
Nivel avanzado
La sentencia match-case va mucho más allá de una simple comparación de valores. Con ella
podremos deconstruir estructuras de datos, capturar elementos o mapear valores.
Para ejemplificar varias de sus funcionalidades, vamos a partir de una tupla que representará
un punto en el plano (2 coordenadas) o en el espacio (3 coordenadas). Lo primero que vamos
a hacer es detectar en qué dimensión se encuentra el punto:
4.1. Condicionales 99
Aprende Python
>>> point = ( 2 , 5 )
Por lo tanto, en un siguiente paso, podemos restringir nuestros patrones a valores enteros:
>>> point = ( 2 , 5 )
Imaginemos ahora que nos piden calcular la distancia del punto al origen. Debemos tener en
cuenta que, a priori, desconocemos si el punto está en el plano o en el espacio:
>>> dist_to_origin
9.899494936611665
Con este enfoque, nos aseguramos que los puntos de entrada deben tener todas sus
coordenadas como valores enteros:
Cambiando de ejemplo, a continuación veremos un código que nos indica si, dada la edad de
una persona, puede beber alcohol:
1 >>> age = 21
2
Nivel avanzado
A partir de Python 3.8 se incorpora el operador morsa5 que permite unificar sentencias de
asignación dentro de expresiones. Su nombre proviene de la forma que adquiere :=
Supongamos un ejemplo en el que computamos el perímetro de una circunferencia, indicando
al usuario que debe incrementarlo siempre y cuando no llegue a un mínimo establecido.
Versión tradicional
Consejo: Como hemos comprobado, el operador morsa permite realizar asignaciones dentro
de expresiones, lo que, en muchas ocasiones, permite obtener un código más compacto. Sería
conveniente encontrar un equilibrio entre la expresividad y la legibilidad.
5
Se denomina así porque el operador := tiene similitud con los colmillos de una morsa.
EJERCICIOS DE REPASO
AMPLIAR CONOCIMIENTOS
4.2 Bucles
Cuando queremos hacer algo más de una vez, necesitamos recurrir a un bucle. En esta
sección veremos las distintas sentencias en Python que nos permiten repetir un bloque de
código.1
El primer mecanismo que existe en Python para repetir instrucciones es usar la sentencia
while. La semántica tras esta sentencia es: «Mientras se cumpla la condición haz algo».
Veamos un sencillo bucle que muestra por pantalla los números del 1 al 4:
>>> value = 1
1
Foto original de portada por Gary Lopater en Unsplash.
>>> num = 20
Comprobar la rotura
Nivel intermedio
Python nos ofrece la posibilidad de detectar si el bucle ha acabado de forma ordinaria,
esto es, ha finalizado por no cumplirse la condición establecida. Para ello podemos hacer uso
de la sentencia else como parte del propio bucle. Si el bucle while finaliza normalmente (sin
llamada a break) el flujo de control pasa a la sentencia opcional else.
Veamos un ejemplo en el que tratamos de encontrar un múltiplo de 9 en el rango [1, 8] (es
obvio que no sucederá):
>>> num = 8
Continuar un bucle
Nivel intermedio
Hay situaciones en las que, en vez de romper un bucle, nos interesa saltar adelante hacia
la siguiente repetición. Para ello Python nos ofrece la sentencia continue que hace
precisamente eso, descartar el resto del código del bucle y saltar a la siguiente iteración.
Veamos un ejemplo en el que usaremos esta estrategia para mostrar todos los números en el
rango [1, 20] ignorando aquellos que sean múltiplos de 3:
>>> num = 21
Bucle infinito
>>> num = 1
El problema que surje es que la variable num toma los valores 1, 3, 5, 7, 9, 11, ..
. por lo que nunca se cumple la condición de parada del bucle. Esto hace que repitamos
«eternamente» la instrucción de incremento.
Ejecución paso a paso a través de Python Tutor:
https://cutt.ly/AfrZroa
Una posible solución a este error es reescribir la condición de parada en el bucle:
>>> num = 1
Truco: Para abortar una situación de bucle infinito podemos pulsar en el teclado
la combinación CTRL-C. Se puede ver reflejado en el intérprete de Python por
KeyboardInterrupt.
Ejercicio
Escriba un programa que encuentre la mínima secuencia de múltiplos de 3 (distintos) cuya
suma sea igual o superior a un valor dado.
Ejemplo
• Entrada: 45
• Salida: 0, 3, 6, 9, 12, 15
Python permite recorrer aquellos tipos de datos que sean iterables, es decir, que admitan
iterar 2 sobre ellos. Algunos ejemplos de tipos y estructuras de datos que permiten ser iteradas
(recorridas) son: cadenas de texto, listas, diccionarios, ficheros, etc. La sentencia for nos
permite realizar esta acción.
A continuación se plantea un ejemplo en el que vamos a recorrer (iterar) una cadena de
texto:
La clave aquí está en darse cuenta que el bucle va tomando, en cada iteración, cada uno de
los elementos de la variable que especifiquemos. En este caso concreto letter va tomando
cada una de las letras que existen en word, porque una cadena de texto está formada por
elementos que son caracteres.
Ejecución paso a paso a través de Python Tutor:
https://cutt.ly/Pft6R2e
Importante: La variable que utilizamos en el bucle for para ir tomando los valores puede
tener cualquier nombre. Al fin y al cabo es una variable que definimos según nuestras
necesidades. Tener en cuenta que se suele usar un nombre en singular.
Una sentencia break dentro de un for rompe el bucle, igual que veíamos para los bucles
while. Veamos un ejemplo con el código anterior. En este caso vamos a recorrer una cadena
de texto y pararemos el bucle cuando encontremos una letra t minúscula:
Ejercicio
Dada una cadena de texto, indique el número de vocales que tiene.
Ejemplo
• Entrada: Supercalifragilisticoespialidoso
• Salida: 15
Secuencias de números
Es muy habitual hacer uso de secuencias de números en bucles. Python no tiene una
instrucción específica para ello. Lo que sí aporta es una función range() que devuelve un
flujo de números en el rango especificado. Una de las grandes ventajas es que la «lista»
generada no se construye explícitamente, sino que cada valor se genera bajo demanda. Esta
técnica mejora el consumo de recursos, especialmente en términos de memoria.
La técnica para la generación de secuencias de números es muy similar a la utilizada en los
«slices» de cadenas de texto. En este caso disponemos de la función range(start, stop,
step):
• start: Es opcional y tiene valor por defecto 0.
• stop: es obligatorio (siempre se llega a 1 menos que este valor).
• step: es opcional y tiene valor por defecto 1.
range() devuelve un objeto iterable, así que iremos obteniendo los valores paso a paso con
una sentencia for ... in3 . Veamos diferentes ejemplos de uso:
Rango: [0, 1, 2]
Rango: [1, 3, 5]
Rango: [2, 1, 0]
antiguos en FORTRAN donde i era la primera letra que tenía valor entero por defecto.
Ejercicio
Determine si un número dado es un número primo.
No es necesario implementar ningún algoritmo en concreto. La idea es probar los números
menores al dado e ir viendo si las divisiones tienen resto cero o no.
¿Podría optimizar su código? ¿Realmente es necesario probar con tantos divisores?
Ejemplo
• Entrada: 11
• Salida: Es primo
Nivel avanzado
Hay situaciones en las que no necesitamos usar la variable que toma valores en el rango,
sino que únicamente queremos repetir una acción un número determinado de veces.
Para estos casos se suele recomendar usar el guión bajo _ como nombre de variable, que
da a entender que no estamos usando esta variable de forma explícita:
Como ya vimos en las sentencias condicionales, el anidamiento es una técnica por la que
incluimos distintos niveles de encapsulamiento de sentencias, unas dentro de otras, con mayor
nivel de profundidad. En el caso de los bucles también es posible hacer anidamiento.
Veamos un ejemplo de 2 bucles anidados en el que generamos todas las tablas de multiplicar:
>>> for i in range(1, 10):
... for j in range(1, 10):
... result = i * j
... print(f {i} * {j} = {result} )
...
1 x 1 = 1
1 x 2 = 2
1 x 3 = 3
1 x 4 = 4
1 x 5 = 5
1 x 6 = 6
1 x 7 = 7
1 x 8 = 8
1 x 9 = 9
2 x 1 = 2
2 x 2 = 4
2 x 3 = 6
2 x 4 = 8
2 x 5 = 10
2 x 6 = 12
2 x 7 = 14
2 x 8 = 16
2 x 9 = 18
3 x 1 = 3
3 x 2 = 6
3 x 3 = 9
3 x 4 = 12
3 x 5 = 15
3 x 6 = 18
3 x 7 = 21
3 x 8 = 24
3 x 9 = 27
4 x 1 = 4
4 x 2 = 8
4 x 3 = 12
4 x 4 = 16
4 x 5 = 20
4 x 6 = 24
4 x 7 = 28
(continué en la próxima página)
Lo que está ocurriendo en este código es que, para cada valor que toma la variable i, la otra
variable j toma todos sus valores. Como resultado tenemos una combinación completa de
los valores en el rango especificado.
Ejecución paso a paso a través de Python Tutor:
https://cutt.ly/vfyeWvj
Nota:
• Podemos añadir todos los niveles de anidamiento que queramos. Eso sí, hay que tener
en cuenta que cada nuevo nivel de anidamiento supone un importante aumento de la
complejidad ciclomática de nuestro código, lo que se traduce en mayores tiempos de
ejecución.
• Los bucles anidados también se pueden aplicar en la sentencia while.
Ejercicio
Imprima los 100 primeros números de la sucesión de Fibonacci:
0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, . . .
EJERCICIOS DE REPASO
4. Escriba un programa en Python que acepte dos cadenas de texto y compute el producto
cartesiano letra a letra entre ellas (solución).
Entrada: cadena1=abc; cadena2=123
Salida: a1 a2 a3 b1 b2 b3 c1 c2 c3
5. Escriba un programa en Python que acepte dos valores enteros (𝑥 e 𝑦) que
representarán un punto (objetivo) en el plano. El programa simulará el movimiento de
un «caballo» de ajedrez moviéndose de forma alterna: 2 posiciones en 𝑥 + 1 posición en
𝑦. El siguiente movimiento que toque sería para moverse 1 posición en 𝑥 + 2 posiciones
en 𝑦. El programa deberá ir mostrando los puntos por los que va pasando el «caballo»
hasta llegar al punto objetivo (solución).
Entrada: objetivo_x=7; objetivo_y=8;
Salida: (0, 0) (1, 2) (3, 3) (4, 5) (6, 6) (7, 8)
6. Escriba un programa que calcule la distancia hamming entre dos cadenas de texto de
la misma longitud (solución).
Entrada: 0001010011101 y 0000110010001
Salida: 4
AMPLIAR CONOCIMIENTOS
Estructuras de datos
Si bien ya hemos visto una sección sobre Tipos de datos, podríamos hablar de tipos de datos
más complejos en Python que se constituyen en estructuras de datos. Si pensamos en
estos elementos como átomos, las estructuras de datos que vamos a ver sería moléculas. Es
decir, combinamos los tipos básicos de formas más complejas. De hecho, esta distinción se
hace en el Tutorial oficial de Python. Trataremos distintas estructuras de datos como listas,
tuplas, diccionarios y conjuntos.
117
Aprende Python
5.1 Listas
Las listas permiten almacenar objetos mediante un orden definido y con posibilidad
de duplicados. Las listas son estructuras de datos mutables, lo que significa que podemos
añadir, eliminar o modificar sus elementos.1
Una lista está compuesta por cero o más elementos. En Python debemos escribir estos
elementos separados por comas y dentro de corchetes. Veamos algunos ejemplos de listas:
>>> empty_list = []
>>> data = [ Tenerife , { cielo : limpio , temp : 24}, 3718, (28.2933947, -16.
˓→5226597)]
Nota: Una lista puede contener tipos de datos heterogéneos, lo que la hace una estructura
1
Foto original de portada por Mike Arney en Unsplash.
5.1.2 Conversión
Para convertir otros tipos de datos en una lista podemos usar la función list():
>>> list(range(10))
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
Lista vacía
Existe una manera particular de usar list() y es no pasarle ningún argumento. En este
caso estaremos queriendo convertir el «vacío» en una lista, con lo que obtendremos una lista
vacía:
>>> list()
[]
Truco: Para crear una lista vacía, se suele recomendar el uso de [] frente a list(), no
sólo por ser más pitónico sino por tener (en promedio) un mejor rendimiento en tiempos de
ejecución.
Obtener un elemento
Igual que en el caso de las cadenas de texto, podemos obtener un elemento de una lista a
través del índice (lugar) que ocupa. Veamos un ejemplo:
>>> shopping = [ Agua , Huevos , Aceite ]
>>> shopping[0]
Agua
>>> shopping[1]
Huevos
>>> shopping[2]
Aceite
El índice que usemos para acceder a los elementos de una lista tiene que estar comprendido
entre los límites de la misma. Si usamos un índice antes del comienzo o después del final
obtendremos un error (excepción):
>>> shopping = [ Agua , Huevos , Aceite ]
>>> shopping[3]
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
IndexError: list index out of range
>>> shopping[-5]
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
IndexError: list index out of range
>>> shopping[0:3]
(continué en la próxima página)
>>> shopping[:3]
[ Agua , Huevos , Aceite ]
>>> shopping[2:4]
[ Aceite , Sal ]
>>> shopping[-1:-4:-1]
[ Limón , Sal , Aceite ]
>>> shopping
[ Agua , Huevos , Aceite , Sal , Limón ]
>>> shopping[10:]
[]
>>> shopping[-100:2]
[ Agua , Huevos ]
>>> shopping[2:100]
[ Aceite , Sal , Limón ]
Python nos ofrece, al menos, tres mecanismos para invertir los elementos de una lista:
Conservando la lista original: Opción 1: Mediante troceado de listas con step negativo:
>>> shopping
[ Agua , Huevos , Aceite , Sal , Limón ]
(continué en la próxima página)
>>> shopping[::-1]
[ Limón , Sal , Aceite , Huevos , Agua ]
>>> shopping
[ Agua , Huevos , Aceite , Sal , Limón ]
>>> list(reversed(shopping))
[ Limón , Sal , Aceite , Huevos , Agua ]
Modificando la lista original: Utilizando la función reverse() (nótese que es sin «d» al
final):
>>> shopping
[ Agua , Huevos , Aceite , Sal , Limón ]
>>> shopping.reverse()
>>> shopping
[ Limón , Sal , Aceite , Huevos , Agua ]
Una de las operaciones más utilizadas en listas es añadir elementos al final de las mismas.
Para ello Python nos ofrece la función append(). Se trata de un método destructivo que
modifica la lista original:
>>> shopping
[ Agua , Huevos , Aceite , Atún ]
Una forma muy habitual de trabajar con listas es empezar con una vacía e ir añadiendo
elementos poco a poco. Se podría hablar de un patrón creación.
Supongamos un ejemplo en el que queremos construir una lista con los números pares del
[0, 20):
>>> even_numbers = []
>>> even_numbers
[0, 2, 4, 6, 8, 10, 12, 14, 16, 18]
Ya hemos visto cómo añadir elementos al final de una lista. Sin embargo, Python ofrece
una función insert() que vendría a ser una generalización de la anterior, para incorporar
elementos en cualquier posición. Simplemente debemos especificar el índice de inserción y el
elemento en cuestión. También se trata de una función destructiva 2 :
>>> shopping
[ Agua , Jamón , Huevos , Aceite ]
>>> shopping
[ Agua , Jamón , Huevos , Queso , Aceite ]
Al igual que ocurría con el troceado de listas, en este tipo de inserciones no obtendremos un
error si especificamos índices fuera de los límites de la lista. Estos se ajustarán al principio
o al final en función del valor que indiquemos:
>>> shopping
[ Agua , Huevos , Aceite , Mermelada ]
>>> shopping
[ Arroz , Agua , Huevos , Aceite , Mermelada ]
Consejo: Aunque es posible utilizar insert() para añadir elementos al final de una
lista, siempre se recomienda usar append() por su mayor legibilidad:
Repetir elementos
Al igual que con las cadenas de texto, el operador * nos permite repetir los elementos de una
lista:
>>> shopping * 3
[ Agua ,
Huevos ,
Aceite ,
(continué en la próxima página)
Combinar listas
>>> shopping.extend(fruitshop)
>>> shopping
[ Agua , Huevos , Aceite , Naranja , Manzana , Piña ]
Hay que tener en cuenta que extend() funciona adecuadamente si pasamos una lista como
argumento. En otro caso, quizás los resultados no sean los esperados. Veamos un ejemplo:
>>> shopping
[ Agua , Huevos , Aceite , L , i , m , ó , n ]
El motivo es que extend() «recorre» (o itera) sobre cada uno de los elementos del objeto
en cuestión. En el caso anterior, al ser una cadena de texto, está formada por caracteres. De
ahí el resultado que obtenemos.
Se podría pensar en el uso de append() para combinar listas. La realidad es que no funciona
exactamente como esperamos; la segunda lista se añadiría como una sublista de la principal:
>>> shopping.append(fruitshop)
>>> shopping
[ Agua , Huevos , Aceite , [ Naranja , Manzana , Piña ]]
Del mismo modo que se accede a un elemento utilizando su índice, también podemos
modificarlo:
>>> shopping[0]
Agua
>>> shopping
[ Jugo , Huevos , Aceite ]
No sólo es posible modificar un elemento de cada vez, sino que podemos asignar valores a
trozos de una lista:
>>> shopping[1:4]
[ Huevos , Aceite , Sal ]
>>> shopping
[ Agua , Atún , Pasta , Limón ]
Nota: La lista que asignamos no necesariamente debe tener la misma longitud que el trozo
que sustituimos.
Borrar elementos
Python nos ofrece, al menos, cuatro formas para borrar elementos en una lista:
Por su índice: Mediante la función del():
>>> del(shopping[3])
>>> shopping
[ Agua , Huevos , Aceite , Limón ]
>>> shopping
[ Agua , Huevos , Aceite , Limón ]
Por su índice (con extracción): Las dos funciones anteriores del() y remove()
efectivamente borran el elemento indicado de la lista, pero no «devuelven»3 nada. Sin
embargo, Python nos ofrece la función pop() que además de borrar, nos «recupera»
el elemento; algo así como una extracción. Lo podemos ver como una combinación de
acceso + borrado:
3
Más adelante veremos el comportamiento de las funciones. Devolver o retornar un valor es el resultado
de aplicar una función.
>>> shopping.pop()
Limón
>>> shopping
[ Agua , Huevos , Aceite , Sal ]
>>> shopping.pop(2)
Aceite
>>> shopping
[ Agua , Huevos , Sal ]
Nota: Si usamos la función pop() sin pasarle ningún argumento, por defecto usará
el índice -1, es decir, el último elemento de la lista. Pero también podemos indicarle el
índice del elemento a extraer.
>>> shopping[1:4] = []
>>> shopping
[ Agua , Limón ]
Python nos ofrece, al menos, dos formas para borrar una lista por completo:
1. Utilizando la función clear():
>>> shopping
[]
>>> shopping
[]
Nota: La diferencia entre ambos métodos tiene que ver con cuestiones internas de gestión
de memoria y de rendimiento.
Encontrar un elemento
Tener en cuenta que si el elemento que buscamos no está en la lista, obtendremos un error:
>>> shopping = [ Agua , Huevos , Aceite , Sal , Limón ]
Nota: Si buscamos un valor que existe más de una vez en una lista, la función index() sólo
nos devolverá el índice de la primera ocurrencia.
Pertenencia de un elemento
Ejercicio
Determine si una cadena de texto dada es un isograma, es decir, no se repite ninguna letra.
Ejemplos válidos de isogramas:
• lumberjacks
• background
• downstream
• six-year-old
Número de ocurrencias
Para contar cuántas veces aparece un determinado valor dentro de una lista podemos usar
la función count():
Dada una lista, podemos convetirla a una cadena de texto, uniendo todos sus elementos
mediante algún separador. Para ello hacemos uso de la función join() con la siguiente
estructura:
>>> , .join(shopping)
Agua,Huevos,Aceite,Sal,Limón
>>> .join(shopping)
Agua Huevos Aceite Sal Limón
>>> | .join(shopping)
Agua|Huevos|Aceite|Sal|Limón
Hay que tener en cuenta que join() sólo funciona si todos sus elementos son cadenas de
texto:
Truco: Esta función join() es realmente la opuesta a la de split() para dividir una
cadena.
Ejercicio
Consiga la siguiente transformación:
12/31/20 31-12-2020
Python proporciona, al menos, dos formas de ordenar los elementos de una lista:
Conservando lista original: Mediante la función sorted() que devuelve una nueva lista
ordenada:
>>> sorted(shopping)
[ Aceite , Agua , Huevos , Limón , Sal ]
>>> shopping.sort()
>>> shopping
[ Aceite , Agua , Huevos , Limón , Sal ]
Ambos métodos admiten un parámetro «booleano» reverse para indicar si queremos que
la ordenación se haga en sentido inverso:
Podemos conocer el número de elementos que tiene una lista con la función len():
>>> len(shopping)
5
Al igual que hemos visto con las cadenas de texto, también podemos iterar sobre los elementos
de una lista utilizando la sentencia for:
Nota: También es posible usar la sentencia break en este tipo de bucles para abortar su
ejecución en algún momento que nos interese.
Hay veces que no sólo nos interesa «visitar» cada uno de los elementos de una lista, sino
que también queremos saber su índice dentro de la misma. Para ello Python nos ofrece la
función enumerate():
Nota: En el caso de que las listas no tengan la misma longitud, la función zip() realiza la
combinación hasta que se agota la lista más corta.
Dado que zip() produce un iterador, si queremos obtener una lista explícita con la
combinación en paralelo de las listas, debemos construir dicha lista de la siguiente manera:
Ejercicio
Dados dos vectores (listas) de la misma dimensión, utilice la función zip() para calcular su
producto escalar.
Ejemplo
• Entrada:
v1 = [4, 3, 8, 1]
v2 = [9, 2, 7, 3]
• Salida: 101
𝑣1 × 𝑣2 = [4 · 9 + 3 · 2 + 8 · 7 + 1 · 3] = 101
Nivel intermedio
Las listas son estructuras de datos mutables y esta característica nos obliga a tener cuidado
cuando realizamos copias de listas, ya que la modificación de una de ellas puede afectar a la
otra.
Veamos un ejemplo sencillo:
>>> original_list[0] = 15
>>> original_list
[15, 3, 7, 1]
>>> copy_list
[15, 3, 7, 1]
Nota: A través de Python Tutor se puede ver claramente el motivo de por qué ocurre esto.
Dado que las variables «apuntan» a la misma zona de memoria, al modificar una de ellas, el
cambio también se ve reflejado en la otra.
Una posible solución a este problema es hacer una «copia dura». Para ello Python
proporciona la función copy():
>>> original_list[0] = 15
>>> original_list
[15, 3, 7, 1]
Truco: En el caso de que estemos trabajando con listas que contienen elementos mutables,
debemos hacer uso de la función deepcopy() dentro del módulo copy de la librería estándar.
Veamos la versión con veracidad múltiple usando all(), donde se comprueba que se
cumplan todas las expresiones:
>>> if is_cool_word:
... print( Cool word! )
... else:
... print( No thanks )
...
Cool word!
Veamos la versión con veracidad múltiple usando any(), donde se comprueba que se
cumpla alguna expresión:
>>> if is_fine_word:
... print( Fine word! )
... else:
... print( No thanks )
...
Fine word!
Consejo: Este enfoque puede ser interesante cuando se manejan muchas condiciones o bien
cuando queremos separar las condiciones y agruparlas en una única lista.
Nivel intermedio
Las listas por comprensión establecen una técnica para crear listas de forma más
compacta basándose en el concepto matemático de conjuntos definidos por comprensión.
En primer lugar veamos un ejemplo en el que convertimos una cadena de texto con valores
numéricos en una lista con los mismos valores pero convertidos a enteros. En su versión
clásica haríamos algo tal que así:
>>> int_values = []
>>> int_values
[32, 45, 11, 87, 20, 48]
>>> int_values
[32, 45, 11, 87, 20, 48]
Condiciones en comprensiones
>>> int_values
[45, 48]
Anidamiento en comprensiones
Nivel avanzado
En la iteración que usamos dentro de la lista por comprensión es posible usar bucles anidados.
Veamos un ejemplo en el que generamos todas las combinaciones de una serie de valores:
>>> combinations
[ 32x32 ,
32x45 ,
32x11 ,
32x87 ,
32x20 ,
32x48 ,
45x32 ,
45x45 ,
...
48x45 ,
48x11 ,
48x87 ,
48x20 ,
48x48 ]
Consejo: Las listas por comprensión son una herramienta muy potente y nos ayuda en
muchas ocasiones, pero hay que tener cuidado de no generar expresiones excesivamente
Ejercicio
Utilizando listas por comprensión, cree una lista que contenga el resultado de aplicar la
función 𝑓 (𝑥) = 3𝑥 + 2 para 𝑥 ∈ [0, 20).
Salida esperada: [2, 5, 8, 11, 14, 17, 20, 23, 26, 29, 32, 35, 38, 41, 44, 47,
50, 53, 56, 59]
5.1.7 sys.argv
1 import sys
2
3 filename = sys.argv[0]
4 arg1 = sys.argv[1]
5 arg2 = float(sys.argv[2])
(continué en la próxima página)
9 print(f {arg1=} )
10 print(f {arg2=} )
11 print(f {arg3=} )
12 print(f {arg4=} )
arg1= hello
arg2=99.9
arg3=55
arg4= a nice arg
Python nos ofrece, entre otras4 , estas tres funciones matemáticas básicas que se pueden
aplicar sobre listas.
Suma de todos los valores: Mediante la función sum():
Ejercicio
4
Existen multitud de paquetes científicos en Python para trabajar con listas o vectores numéricos. Una
de las más famosas es la librería Numpy.
Lea desde línea de comandos una serie de números y obtenga la media de dichos valores
(muestre el resultado con 2 decimales).
La llamada se haría de la siguiente manera:
$ python3 avg.py 32 56 21 99 12 17
Ejemplo
• Entrada: 32 56 21 99 12 17
• Salida: 39.50
Nivel intermedio
Como ya hemos visto en varias ocasiones, las listas son estructuras de datos que pueden
contener elementos heterogéneos. Estos elementos pueden ser a su vez listas.
A continuación planteamos un ejemplo del mundo deportivo. Un equipo de fútbol suele tener
una disposición en el campo organizada en líneas de jugadores. En aquella alineación con la
que España ganó la copa del mundo en 2010 había una disposición 4-3-3 con los siguientes
jugadores:
Veamos una posible representación de este equipo de fútbol usando una lista compuesta de
listas. Primero definimos cada una de las líneas:
>>> goalkeeper = Casillas
>>> defenders = [ Capdevila , Piqué , Puyol , Ramos ]
>>> midfielders = [ Xabi , Busquets , X. Alonso ]
>>> forwards = [ Iniesta , Villa , Pedro ]
>>> team
(continué en la próxima página)
Ejercicio
Escriba un programa que permita multiplicar únicamente matrices de 2 filas por 2 columnas.
Veamos un ejemplo concreto:
EJERCICIOS DE REPASO
1. Escriba un programa en Python que acepte una lista de valores numéricos y obtenga
su valor máximo sin utilizar la función «built-in» max() (solución).
Entrada: [6, 3, 9, 2, 10, 31, 15, 7]
Salida: 31
2. Escriba un programa en Python que acepte una lista y elimine sus elementos duplicados
(solución).
Entrada: [“this”, “is”, “a”, “real”, “real”, “real”, “story”]
Salida: [“this”, “is”, “a”, “real”, “story”]
3. Escriba un programa en Python que acepte una lista – que puede contener sublistas
(sólo en 1 nivel de anidamiento) – y genere otra lista «aplanada» (solución).
Entrada: [0, 10, [20, 30], 40, 50, [60, 70, 80], [90, 100, 110, 120]]
Salida: [0, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100, 110, 120]
4. Escriba un programa en Python que acepte una lista y genere otra lista eliminando los
elementos duplicados consecutivos (solución).
Entrada: [0, 0, 1, 2, 3, 4, 4, 5, 6, 6, 6, 7, 8, 9, 4, 4]
Salida: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 4]
5. Escriba un programa en Python que acepte una lista de listas representando una matriz
numérica y compute la suma de los elementos de la diagonal principal (solución).
Entrada: [[4, 6, 1], [2, 9, 3], [1, 7, 7]]
Salida: 20
AMPLIAR CONOCIMIENTOS
5.2 Tuplas
El concepto de tupla es muy similar al de lista. Aunque hay algunas diferencias menores, lo
fundamental es que, mientras una lista es mutable y se puede modificar, una tupla no admite
cambios y por lo tanto, es inmutable.1
Podemos pensar en crear tuplas tal y como lo hacíamos con listas, pero usando paréntesis
en lugar de corchetes:
>>> empty_tuple = ()
1
Foto original de portada por engin akyurt en Unsplash.
Tuplas de un elemento
Hay que prestar especial atención cuando vamos a crear una tupla de un único elemento.
La intención primera sería hacerlo de la siguiente manera:
>>> one_item_tuple
Papá Noel
>>> type(one_item_tuple)
str
Realmente, hemos creado una variable de tipo str (cadena de texto). Para crear una tupla
de un elemento debemos añadir una coma al final:
>>> one_item_tuple
( Papá Noel ,)
>>> type(one_item_tuple)
tuple
Según el caso, hay veces que nos podemos encontrar con tuplas que no llevan paréntesis.
Quizás no está tan extendido, pero a efectos prácticos tiene el mismo resultado. Veamos
algunos ejemplos de ello:
Como ya hemos comentado previamente, las tuplas con estructuras de datos inmutables.
Una vez que las creamos con un valor, no podemos modificarlas. Veamos qué ocurre si lo
intentamos:
5.2.3 Conversión
Para convertir otros tipos de datos en una tupla podemos usar la función tuple():
>>> tuple(shopping)
( Agua , Aceite , Arroz )
Esta conversión es válida para aquellos tipos de datos que sean iterables: cadenas de
caracteres, listas, diccionarios, conjuntos, etc. Un ejemplo que no funciona es intentar
convertir un número en una tupla:
>>> tuple(5)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: int object is not iterable
El uso de la función tuple() sin argumentos equivale a crear una tupla vacía:
>>> tuple()
()
Truco: Para crear una tupla vacía, se suele recomendar el uso de () frente a tuple(), no
sólo por ser más pitónico sino por tener (en promedio) un mejor rendimiento en tiempos de
ejecución.
Con las tuplas podemos realizar todas las operaciones que vimos con listas salvo las que
conlleven una modificación «in-situ» de la misma:
• reverse()
• append()
• extend()
• remove()
• clear()
• sort()
El desempaquetado es una característica de las tuplas que nos permite asignar una tupla
a variables independientes:
>>> king1
Melchor
>>> king2
Gaspar
(continué en la próxima página)
>>> quotient
2
>>> remainder
1
Intercambio de valores
A través del desempaquetado de tuplas podemos llevar a cabo el intercambio de los valores
de dos variables de manera directa:
>>> value1 = 40
>>> value2 = 20
>>> value1
20
>>> value2
40
Nota: A priori puede parecer que esto es algo «natural», pero en la gran mayoría de lenguajes
de programación no es posible hacer este intercambio de forma «directa» ya que necesitamos
recurrir a una tercera variable «auxiliar» como almacén temporal en el paso intermedio de
traspaso de valores.
Desempaquetado extendido
No tenemos que ceñirnos a realizar desempaquetado uno a uno. También podemos extenderlo
e indicar ciertos «grupos» de elementos mediante el operador *.
Veamos un ejemplo:
>>> ranking = ( G , A , R , Y , W )
>>> head
G
>>> body
[ A , R , Y ]
>>> tail
W
Desempaquetado genérico
El desempaquetado de tuplas es extensible a cualquier tipo de datos que sea iterable. Veamos
algunos ejemplos de ello.
Sobre cadenas de texto:
>>> oxygen = O2
>>> first, last = oxygen
>>> first, last
( O , 2 )
Sobre listas:
>>> writer1, writer2, writer3 = [ Virginia Woolf , Jane Austen , Mary Shelley ]
>>> writer1, writer2, writer3
( Virginia Woolf , Jane Austen , Mary Shelley )
Sin embargo no hemos conseguido una tupla por comprensión sino un generador:
>>> myrange
<generator object <genexpr> at 0x10b3732e0>
Aunque puedan parecer estructuras de datos muy similares, sabemos que las tuplas carecen de
ciertas operaciones, especialmente las que tienen que ver con la modificación de sus valores,
ya que no son inmutables. Si las listas son más flexibles y potentes, ¿por qué íbamos a
necesitar tuplas? Veamos 4 potenciales ventajas del uso de tuplas frente a las listas:
1. Las tuplas ocupan menos espacio en memoria.
2. En las tuplas existe protección frente a cambios indeseados.
3. Las tuplas se pueden usar como claves de diccionarios (son «hashables»).
4. Las namedtuples son una alternativa sencilla a los objetos.
5.3 Diccionarios
Nota: En otros lenguajes de programación, a los diccionarios se les conoce como arrays
1
Foto original de portada por Aaron Burden en Unsplash.
2
Aunque históricamente Python no establecía que las claves de los diccionarios tuvieran que mantener su
orden de inserción, a partir de Python 3.7 este comportamiento cambió y se garantizó el orden de inserción
de las claves como parte oficial de la especificación del lenguaje.
3
Véase este análisis de complejidad y rendimiento de distintas estructuras de datos en CPython.
Para crear un diccionario usamos llaves {} rodeando asignaciones clave: valor que están
separadas por comas. Veamos algunos ejemplos de diccionarios:
>>> empty_dict = {}
>>> rae = {
... bifronte : De dos frentes o dos caras ,
... anarcoide : Que tiende al desorden ,
... montuvio : Campesino de la costa
... }
>>> population_can = {
... 2015: 2_135_209,
... 2016: 2_154_924,
... 2017: 2_177_048,
... 2018: 2_206_901,
... 2019: 2_220_270
... }
En el código anterior podemos observar la creación de un diccionario vacío, otro donde sus
claves y sus valores son cadenas de texto y otro donde las claves y los valores son valores
enteros.
Ejecución paso a paso a través de Python Tutor:
https://cutt.ly/Sfav2Yw
Ejercicio
Cree un diccionario con los nombres de 5 personas de su familia y sus edades.
5.3.2 Conversión
Para convertir otros tipos de datos en un diccionario podemos usar la función dict():
Nota: Si nos fijamos bien, cualquier iterable que tenga una estructura interna de 2 elementos
es susceptible de convertirse en un diccionario a través de la función dict().
Diccionario vacío
Existe una manera particular de usar dict() y es no pasarle ningún argumento. En este
caso estaremos queriendo convertir el «vacío» en un diccionario, con lo que obtendremos un
diccionario vacío:
>>> dict()
{}
Truco: Para crear un diccionario vacío, se suele recomendar el uso de {} frente a dict(),
no sólo por ser más pitónico sino por tener (en promedio) un mejor rendimiento en tiempos
de ejecución.
También es posible utilizar la función dict() para crear dicionarios y no tener que utilizar
llaves y comillas:
Supongamos que queremos transformar la siguiente tabla en un diccionario:
Atributo Valor
name Guido
surname Van Rossum
job Python creator
Utilizando la construcción mediante dict podemos pasar clave y valor como argumentos
de la función:
>>> person
{ name : Guido , surname : Van Rossum , job : Python creator }
El inconveniente que tiene esta aproximación es que las claves deben ser identificadores
válidos en Python. Por ejemplo, no se permiten espacios:
Nivel intermedio
Es posible crear un diccionario especificando sus claves y un único valor de «relleno»:
Obtener un elemento
Para obtener un elemento de un diccionario basta con escribir la clave entre corchetes.
Veamos un ejemplo:
>>> rae = {
... bifronte : De dos frentes o dos caras ,
... anarcoide : Que tiende al desorden ,
... montuvio : Campesino de la costa
... }
Usando get()
Existe una función muy útil para «superar» los posibles errores de acceso por claves
inexistentes. Se trata de get() y su comportamiento es el siguiente:
1. Si la clave que buscamos existe, nos devuelve su valor.
2. Si la clave que buscamos no existe, nos devuelve None4 salvo que le indiquemos otro
valor por defecto, pero en ninguno de los dos casos obtendremos un error.
1 >>> rae
2 { bifronte : De dos frentes o dos caras ,
3 anarcoide : Que tiende al desorden ,
4 montuvio : Campesino de la costa }
5
>>> rae = {
... bifronte : De dos frentes o dos caras ,
... anarcoide : Que tiende al desorden ,
... montuvio : Campesino de la costa
... }
>>> rae
{ bifronte : De dos frentes o dos caras ,
anarcoide : Que tiende al desorden ,
montuvio : Campesino de la costa ,
enjuiciar : Someter una cuestión a examen, discusión y juicio }
Supongamos ahora que queremos modificar el significado de la palabra enjuiciar por otra
acepción:
>>> rae
{ bifronte : De dos frentes o dos caras ,
anarcoide : Que tiende al desorden ,
montuvio : Campesino de la costa ,
enjuiciar : Instruir, juzgar o sentenciar una causa }
Ejercicio
Construya un diccionario partiendo de una cadena de texto con el siguiente formato:
<city>:<population>;<city>:<population>;<city>:<population>;....
• Claves: ciudades.
• Valores: habitantes (como enteros).
Ejemplo
• Entrada: Tokyo:38_140_000;Delhi:26_454_000;Shanghai:24_484_000;
Mumbai:21_357_000;São Paulo:21_297_000
• Salida: { Tokyo : 38140000, Delhi : 26454000, Shanghai : 24484000,
Mumbai : 21357000, São Paulo : 21297000}
Una forma muy habitual de trabajar con diccionarios es utilizar el patrón creación
partiendo de uno vacío e ir añadiendo elementos poco a poco.
Supongamos un ejemplo en el que queremos construir un diccionario donde las claves son
las letras vocales y los valores son sus posiciones:
>>> enum_vowels = {}
>>> enum_vowels
{ a : 1, e : 2, i : 3, o : 4, u : 5}
Nota: Hemos utilizando la función enumerate() que ya vimos para las listas en el apartado:
Iterar usando enumeración.
Ejercicio
Usando un diccionario, cuente el número de veces que se repite cada letra en una cadena de
texto dada.
Ejemplo
• Entrada: boom
• Salida: { b : 1, o : 2, m : 1}
Python ofrece mecanismos para obtener todos los elementos de un diccionario. Partimos del
siguiente diccionario:
>>> rae
{ bifronte : De dos frentes o dos caras ,
anarcoide : Que tiende al desorden ,
montuvio : Campesino de la costa ,
enjuiciar : Instruir, juzgar o sentenciar una causa }
>>> rae.keys()
dict_keys([ bifronte , anarcoide , montuvio , enjuiciar ])
>>> rae.values()
dict_values([
De dos frentes o dos caras ,
Que tiende al desorden ,
Campesino de la costa ,
Instruir, juzgar o sentenciar una causa
])
>>> rae.items()
dict_items([
( bifronte , De dos frentes o dos caras ),
( anarcoide , Que tiende al desorden ),
( montuvio , Campesino de la costa ),
( enjuiciar , Instruir, juzgar o sentenciar una causa )
])
Nota: Para este último caso cabe destacar que los «items» se devuelven como una lista
de tuplas, donde cada tupla tiene dos elementos: el primero representa la clave y el segundo
representa el valor.
Longitud de un diccionario
>>> rae
{ bifronte : De dos frentes o dos caras ,
anarcoide : Que tiende al desorden ,
montuvio : Campesino de la costa ,
enjuiciar : Instruir, juzgar o sentenciar una causa }
>>> len(rae)
4
En base a los elementos que podemos obtener, Python nos proporciona tres maneras de iterar
sobre un diccionario.
Iterar sobre claves:
Nota: En este último caso, recuerde el uso de los «f-strings» para formatear cadenas de
texto.
Ejercicio
Dado el diccionario de ciudades y poblaciones ya visto, y suponiendo que estas ciudades son
las únicas que existen en el planeta, calcule el porcentaje de población relativo de cada una
de ellas con respecto al total.
Ejemplo
• Entrada: Tokyo:38_140_000;Delhi:26_454_000;Shanghai:24_484_000;
Mumbai:21_357_000;São Paulo:21_297_000
• Salida: { Tokyo : 28.952722193544467, Delhi : 20.081680988673973,
Shanghai : 18.58622050830474, Mumbai : 16.212461664591746, São Paulo :
16.16691464488507}
Combinar diccionarios
Dados dos (o más) diccionarios, es posible «mezclarlos» para obtener una combinación de
los mismos. Esta combinación se basa en dos premisas:
1. Si la clave no existe, se añade con su valor.
2. Si la clave ya existe, se añade con el valor del «último» diccionario en la mezcla.6
Python ofrece dos mecanismos para realizar esta combinación. Vamos a partir de los
siguientes diccionarios para ejemplificar su uso:
>>> rae1 = {
... bifronte : De dos frentes o dos caras ,
... enjuiciar : Someter una cuestión a examen, discusión y juicio
... }
>>> rae2 = {
... anarcoide : Que tiende al desorden ,
... montuvio : Campesino de la costa ,
... enjuiciar : Instruir, juzgar o sentenciar una causa
... }
A partir de Python 3.9 podemos utilizar el operador | para combinar dos diccionarios:
>>> rae1.update(rae2)
>>> rae1
{ bifronte : De dos frentes o dos caras ,
enjuiciar : Instruir, juzgar o sentenciar una causa ,
(continué en la próxima página)
6
En este caso «último» hace referencia al diccionario que se encuentra más a la derecha en la expresión.
Nota: Tener en cuenta que el orden en el que especificamos los diccionarios a la hora de su
combinación (mezcla) es relevante en el resultado final. En este caso el orden de los factores
sí altera el producto.
Borrar elementos
Python nos ofrece, al menos, tres formas para borrar elementos en un diccionario:
Por su clave: Mediante la sentencia del:
>>> rae = {
... bifronte : De dos frentes o dos caras ,
... anarcoide : Que tiende al desorden ,
... montuvio : Campesino de la costa
... }
>>> rae
{ anarcoide : Que tiende al desorden , montuvio : Campesino de la costa }
Por su clave (con extracción): Mediante la función pop() podemos extraer un elemento
del diccionario por su clave. Vendría a ser una combinación de get() + del:
>>> rae = {
... bifronte : De dos frentes o dos caras ,
... anarcoide : Que tiende al desorden ,
... montuvio : Campesino de la costa
... }
>>> rae
{ bifronte : De dos frentes o dos caras , montuvio : Campesino de la costa }
>>> rae = {
... bifronte : De dos frentes o dos caras ,
... anarcoide : Que tiende al desorden ,
... montuvio : Campesino de la costa
... }
>>> rae.clear()
>>> rae
{}
>>> rae = {
... bifronte : De dos frentes o dos caras ,
... anarcoide : Que tiende al desorden ,
... montuvio : Campesino de la costa
... }
>>> rae = {}
>>> rae
{}
Nota: La diferencia entre ambos métodos tiene que ver con cuestiones internas de
gestión de memoria y de rendimiento.
Nivel intermedio
Al igual que ocurría con las listas, si hacemos un cambio en un diccionario, se verá reflejado
en todas las variables que hagan referencia al mismo. Esto se deriva de su propiedad de ser
mutable. Veamos un ejemplo concreto:
>>> original_rae = {
... bifronte : De dos frentes o dos caras ,
... anarcoide : Que tiende al desorden ,
... montuvio : Campesino de la costa
... }
>>> original_rae
{ bifronte : bla bla bla ,
anarcoide : Que tiende al desorden ,
montuvio : Campesino de la costa }
>>> copy_rae
{ bifronte : bla bla bla ,
anarcoide : Que tiende al desorden ,
montuvio : Campesino de la costa }
Una posible solución a este problema es hacer una «copia dura». Para ello Python
proporciona la función copy():
>>> original_rae = {
... bifronte : De dos frentes o dos caras ,
... anarcoide : Que tiende al desorden ,
... montuvio : Campesino de la costa
... }
>>> original_rae
{ bifronte : bla bla bla ,
anarcoide : Que tiende al desorden ,
montuvio : Campesino de la costa }
>>> copy_rae
{ bifronte : De dos frentes o dos caras ,
anarcoide : Que tiende al desorden ,
montuvio : Campesino de la costa }
Truco: En el caso de que estemos trabajando con diccionarios que contienen elementos
mutables, debemos hacer uso de la función deepcopy() dentro del módulo copy de la librería
estándar.
Nivel intermedio
De forma análoga a cómo se escriben las listas por comprensión, podemos aplicar este método
a los diccionarios usando llaves { }.
Veamos un ejemplo en el que creamos un diccionario por comprensión donde las claves
son palabras y los valores son sus longitudes:
>>> words_length
{ sun : 3, space : 5, rocket : 6, earth : 5}
>>> words_length
{ sun : 3, space : 5, rocket : 6}
Nota: Se puede consultar el PEP-274 para ver más ejemplos sobre diccionarios por
comprensión.
Nivel avanzado
La única restricción que deben cumplir las claves de un diccionario es ser «hashables»7 .
Un objeto es «hashable» si se le puede asignar un valor «hash» que no cambia en ejecución
durante toda su vida.
7
Se recomienda esta ponencia de Víctor Terrón sobre objetos «hashables».
Para encontrar el «hash» de un objeto, Python usa la función hash(), que devuelve un
número entero y es utilizado para indexar la tabla «hash» que se mantiene internamente:
>>> hash(999)
999
>>> hash(3.14)
322818021289917443
>>> hash(( a , b , c ))
-2157188727417140402
>>> hash([ a , b , c ])
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: unhashable type: list
Nota: De lo anterior se deduce que las claves de los diccionarios, al tener que ser
«hasheables», sólo pueden ser objetos inmutables.
La función «built-in» hash() realmente hace una llamada al método mágico __hash__() del
objeto en cuestión:
EJERCICIOS DE REPASO
1. Escriba un programa en Python que acepte una lista de palabras y las agrupe por su
letra inicial usando un diccionario (solución).
Entrada: [ “mesa”, “móvil”, “barco”, “coche”, “avión”, “bandeja”, “casa”,
“monitor”, “carretera”, “arco”]
Salida: {“m”: [“mesa”, “móvil”, “monitor”], “b”: [“barco”, “bandeja”], “c”:
[“coche”, “casa”, “carretera”], “a”: [“avión”, “arco”]}
AMPLIAR CONOCIMIENTOS
5.4 Conjuntos
Un conjunto en Python representa una serie de valores únicos y sin orden establecido,
con la única restricción de que sus elementos deben ser «hashables». Mantiene muchas
similitudes con el concepto matemático de conjunto1
Para crear un conjunto basta con separar sus valores por comas y rodearlos de llaves {}:
>>> lottery
{10, 21, 29, 31, 46, 94}
>>> wrong_empty_set = {}
>>> type(wrong_empty_set)
dict
1
Foto original de portada por Duy Pham en Unsplash.
>>> empty_set
set()
>>> type(empty_set)
set
5.4.2 Conversión
Para convertir otros tipos de datos en un conjunto podemos usar la función set() sobre
cualquier iterable:
Importante: Como se ha visto en los ejemplos anteriores, set() se suele utilizar en muchas
ocasiones como una forma de extraer los valores únicos de otros tipos de datos. En el
caso de los diccionarios se extraen las claves, que, por definición, son únicas.
Nota: El hecho de que en los ejemplos anteriores los elementos de los conjuntos estén
ordenados es únicamente un «detalle de implementación» en el que no se puede confiar.
Obtener un elemento
En un conjunto no existe un orden establecido para sus elementos, por lo tanto no podemos
acceder a un elemento en concreto.
De este hecho se deriva igualmente que no podemos modificar un elemento existente,
ya que ni siquiera tenemos acceso al mismo. Python sí nos permite añadir o borrar elementos
de un conjunto.
Añadir un elemento
Para añadir un elemento a un conjunto debemos utilizar la función add(). Como ya hemos
indicado, al no importar el orden dentro del conjunto, la inserción no establece a priori la
posición donde se realizará.
A modo de ejemplo, vamos a partir de un conjunto que representa a los cuatro integrantes
originales de The Beatles. Luego añadiremos a un nuevo componente:
>>> beatles
{ Best , Harrison , Lennon , McCartney , Starr }
Ejercicio
Dada una tupla de duplas (2 valores), cree dos conjuntos:
• Uno de ellos con los primeros valores de cada dupla.
• El otro con los segundos valores de cada dupla.
Ejemplo
• Entrada: ((4, 3), (8, 2), (7, 5), (8, 2), (9, 1))
• Salida:
{8, 9, 4, 7}
{1, 2, 3, 5}
Borrar elementos
>>> beatles
{ Best , Harrison , Lennon , McCartney , Starr }
>>> beatles
{ Harrison , Lennon , McCartney , Starr }
Longitud de un conjunto
Podemos conocer el número de elementos que tiene un conjunto con la función len():
>>> beatles
{ Harrison , Lennon , McCartney , Starr }
>>> len(beatles)
4
Tal y como hemos visto para otros tipos de datos iterables, la forma de recorrer los elementos
de un conjunto es utilizar la sentencia for:
Consejo: Como en el ejemplo anterior, es muy común utilizar una variable en singular para
recorrer un iterable (en plural). No es una regla fija ni sirve para todos los casos, pero sí
suele ser una buena práctica.
Pertenencia de elemento
Al igual que con otros tipos de datos, Python nos ofrece el operador in para determinar si
un elemento pertenece a un conjunto:
>>> beatles
{ Harrison , Lennon , McCartney , Starr }
Vamos a partir de dos conjuntos 𝐴 = {1, 2} y 𝐵 = {2, 3} para ejemplificar las distintas
operaciones que se pueden hacer entre ellos basadas en los Diagramas de Venn y la Teoría
de Conjuntos:
>>> A = {1, 2}
>>> B = {2, 3}
Intersección
>>> A & B
{2}
>>> A.intersection(B)
{2}
Unión
>>> A | B
{1, 2, 3}
>>> A.union(B)
{1, 2, 3}
Diferencia
>>> A - B
{1}
>>> A.difference(B)
{1}
Diferencia simétrica
>>> A ^ B
{1, 3}
>>> A.symmetric_difference(B)
{1, 3}
Los conjuntos, al igual que las listas y los diccionarios, también se pueden crear por
comprensión.
Veamos un ejemplo en el que construimos un conjunto por comprensión con los aquellos
números enteros múltiplos de 3 en el rango [0, 20):
>>> m3
{0, 3, 6, 9, 12, 15, 18}
Ejercicio
Dadas dos cadenas de texto, obtenga una nueva cadena de texto con las letras consonantes
que se repiten en ambas frases. Ignore los espacios en blanco y muestre la cadena de salida
con sus letras ordenadas.
Resuelva el ejercicio mediante dos aproximaciones: Una de ellas usando conjuntos por
comprensión y otra sin usar comprensiones.
Ejemplo
• Entrada: Flat is better than nested y Readability counts
• Salida: bdlnst
>>> marks_levels
frozenset({1, 2, 3, 4, 5})
>>> marks_levels.add(50)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: frozenset object has no attribute add
Nota: Los frozenset son a los sets lo que las tuplas a las listas: una forma de «congelar»
los valores para que no se puedan modificar.
AMPLIAR CONOCIMIENTOS
• Sets in Python
5.5 Ficheros
Python ofrece la función open() para «abrir» un fichero. Esta apertura se puede realizar en
3 modos distintos:
• Lectura del contenido de un fichero existente.
• Escritura del contenido en un fichero nuevo.
• Añadido al contenido de un fichero existente.
Veamos un ejemplo para leer el contenido de un fichero en el que se encuentran las
temperaturas máximas y mínimas de cada día de la última semana. El fichero está en la
subcarpeta (ruta relativa) files/temps.dat y tiene el siguiente contenido:
29 23
31 23
34 26
33 23
29 22
28 22
28 22
La función open() recibe como primer argumento la ruta al fichero que queremos manejar
(como un «string») y devuelve el manejador del fichero, que en este caso lo estamos asignando
a una variable llamada f pero le podríamos haber puesto cualquier otro nombre.
Nota: Es importante dominar los conceptos de ruta relativa y ruta absoluta para el
trabajo con ficheros. Véase este artículo de DeNovatoANovato.
Hay que tener en cuenta que la ruta al fichero que abrimos (en modo lectura) debe existir,
ya que de lo contrario obtendremos un error:
Una vez abierto el fichero ya podemos proceder a leer su contenido. Para ello Python nos
ofrece la posibilidad de leer todo el fichero de una vez o bien leerlo línea a línea.
Siguiendo con nuestro ejemplo de temperaturas, veamos cómo leer todo el contenido del
fichero de una sola vez. Para esta operación, Python nos provee, al menos, de dos funciones:
read() Devuelve todo el contenido del fichero como una cadena de texto (str):
>>> f.read()
29 23\n31 23\n34 26\n33 23\n29 22\n28 22\n28 22\n
readlines() Devuelve todo el contenido del fichero como una lista (list) donde cada
elemento es una línea:
>>> f.readlines()
[ 29 23\n , 31 23\n , 34 26\n , 33 23\n , 29 22\n , 28 22\n , 28 22\n ]
Importante: Nótese que, en ambos casos, los saltos de línea \n siguen apareciendo en
los datos leídos, por lo que habría que «limpiar» estos caracteres. Para ello se recomienda
utilizar las funciones ya vistas de cadenas de texto.
Hay situaciones en las que interesa leer el contenido del fichero línea a línea. Imaginemos un
fichero de tamaño considerable (varios GB). Si intentamos leer completamente este fichero
de sola una vez podríamos ocupar demasiada RAM y reducir el rendimiento de nuestra
máquina.
Es por ello que Python nos ofrece varias aproximaciones a la lectura de ficheros línea a línea.
La más usada es iterar sobre el propio manejador del fichero:
31 23
34 26
(continué en la próxima página)
33 23
29 22
28 22
28 22
Truco: Igual que pasaba anteriormente, la lectura línea por línea también incluye el salto
de línea \n lo que provoca un «doble espacio» entre cada una de las salidas. Bastaría con
aplicar line.split() para eliminarlo.
Para escribir texto en un fichero hay que abrir dicho fichero en modo escritura. Para ello
utilizamos un argumento adicional en la función open() que indica esta operación:
Nota: Si bien el fichero en sí mismo se crea al abrirlo en modo escritura, la ruta hasta
ese fichero no. Eso quiere decir que debemos asegurarnos que las carpetas hasta llegar a
dicho fichero existen. En otro caso obtenemos un error de tipo FileNotFoundError.
Ahora ya podemos hacer uso de la función write() para enviar contenido al fichero abierto.
Supongamos que queremos volcar el contenido de una lista en dicho fichero. En este caso
partimos de los códigos IATA de aeropuertos de las Islas Canarias2 .
1 >>> canary_iata = ("GCFV", "GCHI", "GCLA", "GCLP", "GCGM", "GCRR", "GCTS", "GCXO")
2
7 >>> f.close()
Nótese:
2
Fuente: Smart Drone
En este caso el fichero more-data.txt se abrirá en modo añadir con lo que las llamadas a
la función write() hará que aparezcan nueva información al final del contenido ya existente
en dicho fichero.
Python ofrece gestores de contexto como una solución para establecer reglas de entrada y
salida a un determinado bloque de código.
En el caso que nos ocupa, usaremos la sentencia with y el contexto creado se ocupará de
cerrar adecuadamente el fichero que hemos abierto, liberando así sus recursos:
Línea 1 Apertura del fichero en modo lectura utilizando el gestor de contexto definido por
la palabra reservada with.
Línea 2 Lectura del fichero línea a línea utilizando la iteración sobre el manejador del
fichero.
Línea 3 Limpieza de saltos de línea con strip() encadenando la función split() para
separar las dos temperaturas por el carácter espacio. Ver limpiar una cadena y dividir
una cadena.
Línea 4 Imprimir por pantalla la temperatura mínima y la máxima.
Nota: Es una buena práctica usar with cuando se manejan ficheros. La ventaja es que el
fichero se cierra adecuadamente en cualquier circunstancia, incluso si se produce cualquier
tipo de error.
Hay que prestar atención a la hora de escribir valores numéricos en un fichero, ya que el
método write() por defecto espera ver un «string» como argumento:
Importante: Para evitar este tipo de errores, se debe convertir a str aquellos valores que
queramos usar con la función write() para escribir información en un fichero de texto.
Ejercicio
Dado el fichero temperatures.txt con 12 filas (meses) y 31 columnas (temperaturas de cada
día), se pide:
1. Leer el fichero de datos.
2. Calcular la temperatura media de cada mes.
3. Escribir un fichero de salida avgtemps.txt con 12 filas (meses) y la temperatura media
de cada mes.
Guarda el fichero en la misma carpeta en la que vas a escribir tu código. Así evitarás problemas
de rutas relativas/absolutas.
AMPLIAR CONOCIMIENTOS
Modularidad
1
Definición de modularidad en Wikipedia
185
Aprende Python
6.1 Funciones
Para definir una función utilizamos la palabra reservada def seguida del nombre6 de
la función. A continuación aparecerán 0 o más parámetros separados por comas (entre
paréntesis), finalizando la línea con dos puntos : En la siguiente línea empezaría el cuerpo
de la función que puede contener 1 o más sentencias, incluyendo (o no) una sentencia de
retorno con el resultado mediante return.
1
Foto original por Nathan Dumlao en Unsplash.
6
Las reglas aplicadas a nombres de variables también se aplican a nombres de funciones.
Advertencia: Prestar especial atención a los dos puntos : porque suelen olvidarse en
la definición de la función.
def say_hello():
print( Hello! )
Para invocar (o «llamar») a una función sólo tendremos que escribir su nombre seguido de
paréntesis. En el caso de la función sencilla (vista anteriormente) se haría así:
>>> say_hello()
Hello!
Como era de esperar, al invocar a esta función obtenemos un mensaje por pantalla, fruto de
la ejecución del cuerpo de la función.
Retornar un valor
Las funciones pueden retornar (o «devolver») un valor. Veamos un ejemplo muy sencillo:
>>> one()
1
Importante: No confundir return con print(). El valor de retorno de una función nos
permite usarlo fuera de su contexto. El hecho de añadir print() al cuerpo de una función
es algo «coyuntural» y no modifica el resultado de la lógica interna.
>>> if one() == 1:
... print( It works! )
... else:
... print( Something is broken )
...
It works!
Si una función no incluye un return de forma explícita, devolverá None de forma implícita:
>>> print(empty())
None
6.1.2 Veracidad
Nivel intermedio
Ya hemos hablado ligeramente sobre la comprobación de veracidad en Python.
Vamos a crear una función propia para comprobar la veracidad de distintos objetos del
lenguaje, y así hacernos una mejor idea de qué cosas son evaluadas a verdadero y cuáles a
falso:
Evaluando a False
>>> truthiness(False)
False is False
>>> truthiness(None)
None is False
>>> truthiness(0)
0 is False
>>> truthiness(0.0)
0.0 is False
>>> truthiness( )
is False
>>> truthiness([])
[] is False
>>> truthiness(())
() is False
>>> truthiness({})
{} is False
Evaluando a True
>>> truthiness(True)
True is True
>>> truthiness(1e-10)
1e-10 is True
>>> truthiness([0])
[0] is True
>>> truthiness( )
is True
>>> truthiness( )
is True
Cuando llamamos a una función con argumentos, los valores de estos argumentos se copian
en los correspondientes parámetros dentro de la función:
Truco: La sentencia pass permite «no hacer nada». Es una especie de «placeholder».
Veamos otra función con dos parámetros y algo más de lógica de negocio:2
>>> _min(7, 9)
7
Ejercicio
Escriba una función en Python que reproduzca lo siguiente:
𝑓 (𝑥, 𝑦) = 𝑥2 + 𝑦 2
Ejemplo
• Entrada: 3 y 4
• Salida: 25
Argumentos posicionales
Parámetro Argumento
vendor AMD
num_cores 8
freq 2.7
Pero es evidente que una clara desventaja del uso de argumentos posicionales es que se
necesita recordar el orden de los argumentos. Un error en la posición de los argumentos
puede causar resultados indeseados:
Argumentos nominales
En esta aproximación los argumentos no son copiados en un orden específico sino que se
asignan por nombre a cada parámetro. Ello nos permite salvar el problema de conocer
cuál es el orden de los parámetros en la definición de la función. Para utilizarlo, basta con
realizar una asignación de cada argumento en la propia llamada a la función.
Veamos la misma llamada que hemos hecho en el ejemplo de construcción de la «cpu» pero
ahora utilizando paso de argumentos nominales:
Se puede ver claramente que el orden de los argumentos no influye en el resultado final:
Pero hay que tener en cuenta que, en este escenario, los argumentos posicionales siempre
deben ir antes que los argumentos nominales. Esto tiene mucho sentido ya que, de hacerlo
así, Python no tendría forma de discernir a qué parámetro corresponde cada argumento:
Es posible especificar valores por defecto en los parámetros de una función. En el caso
de que no se proporcione un valor al argumento en la llamada a la función, el parámetro
correspondiente tomará el valor definido por defecto.
Siguiendo con el ejemplo de la «cpu», podemos asignar 2.0GHz como frecuencia por defecto.
La definición de la función cambiaría ligeramente:
Importante: Los valores por defecto en los parámetros se calculan cuando se define la
función, no cuando se ejecuta.
Ejercicio
Escriba una función factorial que reciba un único parámetro n y devuelva su factorial.
El factorial de un número n se define como:
𝑛! = 𝑛 · (𝑛 − 1) · (𝑛 − 2) · . . . · 1
Ejemplo
• Entrada: 5
• Salida: 120
Nivel avanzado
Hay que tener cuidado a la hora de manejar los parámetros que pasamos a una función ya
que podemos obtener resultados indeseados, especialmente cuando trabajamos con tipos de
datos mutables.
Supongamos una función que añade elementos a una lista que pasamos por parámetro. La
idea es que si no pasamos la lista, ésta siempre empiece siendo vacía. Hagamos una serie de
pruebas pasando alguna lista como segundo argumento:
>>> buggy( a , [ x , y , z ])
[ x , y , z , a ]
>>> buggy( b , [ x , y , z ])
[ x , y , z , b ]
Aparentemente todo está funcionando de manera correcta, pero veamos qué ocurre en las
siguientes llamadas:
>>> buggy( a )
[ a ]
Obviamente algo no ha funcionado correctamente. Se esperaría que result tuviera una lista
vacía en cada ejecución. Sin embargo esto no sucede por estas dos razones:
1. El valor por defecto se establece cuando se define la función.
2. La variable result apunta a una zona de memoria en la que se modifican sus valores.
Ejecución paso a paso a través de Python Tutor:
https://cutt.ly/MgoQGU3
A riesgo de perder el parámetro por defecto, una posible solución sería la siguiente:
>>> works( a )
[ a ]
>>> works( b )
[ b ]
La forma de arreglar el código anterior utilizando un parámetro con valor por defecto sería
utilizar un tipo de dato inmutable y tener en cuenta cuál es la primera llamada:
>>> nonbuggy( a )
[ a ]
>>> nonbuggy( b )
[ b ]
>>> nonbuggy( a , [ x , y , z ])
[ x , y , z , a ]
>>> nonbuggy( b , [ x , y , z ])
[ x , y , z , b ]
Empaquetar/Desempaquetar argumentos
Nivel avanzado
Python nos ofrece la posibilidad de empaquetar y desempaquetar argumentos cuando estamos
invocando a una función, tanto para argumentos posicionales como para argumentos
nominales.
Y de este hecho se deriva que podamos utilizar un número variable de argumentos en
una función, algo que puede ser muy interesante según el caso de uso que tengamos.
>>> test_args()
args=()
También podemos utilizar esta estrategia para establecer en una función una serie de
parámetros como requeridos y recibir el resto de argumentos como opcionales y empaquetados:
>>> sum_all()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: sum_all() missing 2 required positional arguments: v1 and v2
>>> sum_all(1, 2)
(continué en la próxima página)
>>> test_kwargs()
kwargs={}
>>> my_kwargs = { a : 4, b : 3, c : 7, d : 9}
Si bien Python nos da flexibilidad para pasar argumentos a nuestras funciones en modo
posicional o nominal, existen opciones para forzar a que dicho paso sea obligatorio en una
determinada modalidad.
Nivel avanzado
A partir de Python 3.8 se ofrece la posibilidad de obligar a que determinados parámetros de
la función sean pasados sólo por posición.
Para ello, en la definición de los parámetros de la función, tendremos que incluir un parámetro
especial / que delimitará el tipo de parámetros. Así, todos los parámetros a la izquierda del
delimitador estarán obligados a ser posicionales:
Ejemplo:
>>> sum_power(3, 4)
7
Nivel avanzado
A partir de Python 3 se ofrece la posibilidad de obligar a que determinados parámetros de
la función sean pasados sólo por nombre.
Para ello, en la definición de los parámetros de la función, tendremos que incluir un parámetro
especial * que delimitará el tipo de parámetros. Así, todos los parámetros a la derecha del
separador estarán obligados a ser nominales:
Ejemplo:
>>> sum_power(3, 4)
7
Si mezclamos las dos estrategias anteriores podemos forzar a que una función reciba
argumentos de un modo concreto.
Continuando con ejemplo anterior, podríamos hacer lo siguiente:
Nivel intermedio
Igual que veíamos en la incidencia de parámetros por defecto con valores mutables, cuando
realizamos modificaciones a los argumentos de una función es importante tener en cuenta si
son mutables (listas, diccionarios, conjuntos, …) o inmutables (tuplas, enteros, flotantes,
cadenas de texto, …) ya que podríamos obtener efectos colaterales no deseados:
>>> fib
[1, 1, 2, 3, 5, 8, 13]
>>> fib #
[1, 1, 2, 3, 25, 8, 13]
Nivel avanzado
Las funciones se pueden utilizar en cualquier contexto de nuestro programa. Son objetos que
pueden ser asignados a variables, usados en expresiones, devueltos como valores de retorno
o pasados como argumentos a otras funciones.
Veamos un primer ejemplo en el que pasamos una función como argumento:
>>> type(success)
function
>>> doit(success)
Yeah!
Veamos un segundo ejemplo en el que pasamos, no sólo una función como argumento, sino
los valores con los que debe operar:
>>> type(repeat_please)
function
6.1.4 Documentación
Ya hemos visto que en Python podemos incluir comentarios para explicar mejor determinadas
zonas de nuestro código.
Del mismo modo podemos (y en muchos casos debemos) adjuntar documentación a la
definición de una función incluyendo una cadena de texto (docstring) al comienzo de su
cuerpo:
>>> help(closest_int)
closest_int(value)
Returns the closest integer to the given value.
The operation is:
1. Compute distance to floor.
2. If distance less than a half, return floor.
Otherwise, return ceil.
>>> closest_int?
Signature: closest_int(value)
Docstring:
Returns the closest integer to the given value.
The operation is:
1. Compute distance to floor.
2. If distance less than a half, return floor.
Otherwise, return ceil.
File: ~/aprendepython/<ipython-input-75-5dc166360da1>
Type: function
Importante: Esto no sólo se aplica a funciones propias, sino a cualquier otra función
definida en el lenguaje.
Nota: Si queremos ver el docstring de una función en «crudo» (sin formatear), podemos
usar <function>.__doc__.
Explicación de parámetros
Sphinx
Dentro del «docstring» podemos escribir con sintaxis reStructured Text – véase por ejemplo
la expresión matemática en el tag :return: – lo que nos proporciona una gran flexibilidad.
Nota: La plataforma Read the Docs aloja la documentación de gran cantidad de proyectos.
En muchos de los casos se han usado «docstrings» con el formato Sphinx visto anteriormente.
Anotación de tipos
Nivel intermedio
Las anotaciones de tipos5 se introdujeron en Python 3.5 y permiten indicar tipos para los
parámetros de una función así como su valor de retorno (aunque también funcionan en
creación de variables).
Veamos un ejemplo en el que creamos una función para dividir una cadena de texto por la
posición especificada en el parámetro:
Como se puede observar, vamos añadiendo los tipos después de cada parámetro utilizando
: como separador. En el caso del valor de retorno usamos el símbolo ->
Quizás la siguiente ejecución pueda sorprender:
Efectivamente como habrás visto, no hemos obtenido ningún error, a pesar de que
estamos pasando como primer argumento una lista en vez de una cadena de texto. Esto
ocurre porque lo que hemos definido es una anotación de tipo, no una declaración de tipo.
Existen herramientas como mypy que sí se encargan de chequear estas situaciones.
5
Conocidos como «type hints» en terminología inglesa.
Nota: Las anotaciones de tipos son una herramienta muy potente y que, usada de
forma adecuada, permite complementar la documentación de nuestro código y aclarar ciertos
aspectos, que a priori, pudieran parecer confusos. Su aplicación estará en función de la
necesidad detectada por parte del equipo de desarrollo.
Nivel avanzado
Funciones interiores
Clausuras
Una clausura (del término inglés «closure») establece el uso de una función interior que se
genera dinámicamente y recuerda los valores de los argumentos con los que fue creada:
>>> m3 = make_multiplier_of(3)
>>> m5 = make_multiplier_of(5)
>>> type(m3)
function
>>> m3(7) # 7 * 3
21
>>> type(m5)
function
>>> m5(8) # 8 * 5
40
>>> type(num_words)
function
>>> num_words
<function __main__.<lambda>(t)>
Veamos otro ejemplo en el que mostramos una tabla con el resultado de aplicar el «and»
lógico mediante una función «lambda» que ahora recibe dos parámetros:
>>> logic_and = lambda x, y: x & y
Las funciones «lambda» son bastante utilizadas como argumentos a otras funciones. Un
ejemplo claro de ello es la función sorted que tiene un parámetro opcional key donde se
define la clave de ordenación.
Veamos cómo usar una función anónima «lambda» para ordenar una tupla de pares
longitud-latitud:
>>> geoloc = (
... (15.623037, 13.258358),
(continué en la próxima página)
Enfoque funcional
map()
Esta función aplica otra función sobre cada elemento de un iterable. Supongamos que
queremos aplicar la siguiente función:
𝑥2
𝑓 (𝑥) = ∀𝑥 ∈ [1, 10]
2
>>> type(map_gen)
map
>>> list(map_gen)
[0.5, 2.0, 4.5, 8.0, 12.5, 18.0, 24.5, 32.0, 40.5, 50.0]
filter()
Esta función selecciona aquellos elementos de un iterable que cumplan una determinada
condición. Supongamos que queremos seleccionar sólo aquellos números impares dentro de
un rango:
>>> type(filter_gen)
filter
>>> list(filter_gen)
[1, 3, 5, 7, 9, 11, 13, 15, 17, 19]
reduce()
Para poder usar esta función debemos usar el módulo functools. Nos permite aplicar una
función dada sobre todos los elementos de un iterable de manera acumulativa. O dicho en
otras palabras, nos permite reducir una función sobre un conjunto de valores. Supongamos
que queremos realizar el producto de una serie de valores aplicando este enfoque:
Consejo: Por cuestiones de legibilidad del código, se suelen preferir las listas por
comprensión a funciones como map() o filter(), aunque cada problema tiene sus propias
características y sus soluciones más adecuadas.
Generadores
Un generador es un objeto que nos permite iterar sobre una secuencia de valores con la
particularidad de no tener que crear explícitamente dicha secuencia. Esta propiedad los hace
idóneos para situaciones en las que el tamaño de las secuencias podría tener un impacto
negativo en el consumo de memoria.
De hecho ya hemos visto algunos generadores y los hemos usado de forma directa. Un ejemplo
es range() que ofrece la posibilidad de crear secuencias de números.
Básicamente existen dos implementaciones de generadores:
• Funciones generadoras.
• Expresiones generadoras.
Funciones generadoras
Las funciones generadoras se escriben como funciones ordinarias con el matiz de incorporar
la sentencia yield que sustituye, de alguna manera, a return. Esta sentencia devuelve el
valor indicado y, a la vez, «congela» el estado de la función para subsiguientes ejecuciones.
Veamos un ejemplo en el que escribimos una función generadora de números pares:
>>> type(evens)
function
>>> list(evens(20))
[0, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20]
Importante: Un detalle muy importante sobre los generadores es que «se agotan». Es decir,
una vez que ya hemos consumido todos sus elementos ya no obtendremos nuevos valores.
Expresiones generadoras
Una expresión generadora es sintácticamente muy similar a una lista por comprensión,
pero utilizamos paréntesis en vez de corchetes. Se podría ver como una versión acortada
de una función generadora.
Podemos tratar de reproducir el ejemplo visto en funciones generadoras en el que creamos
números pares hasta el 20:
>>> type(evens_gen)
generator
Ejercicio
Escriba una función generadora que devuelva los 100 primeros números enteros elevados
al cuadrado.
Decoradores
>>> type(simple_logger)
function
Ahora vamos a definir una función ordinaria (que usaremos más adelante):
>>> def hi(name):
... return f Hello {name}!
...
Python nos ofrece un «syntactic sugar» para simplificar la aplicación de los decoradores a
través del operador @ justo antes de la definición de la función que queremos decorar:
>>> @simple_logger
... def hi(name):
... return f Hello {name}!
...
...
Podemos aplicar más de un decorador a cada función. Para ejemplificarlo vamos a crear dos
decoradores muy sencillos:
Ahora aplicaremos ambos decoradores sobre una función que realiza el producto de dos
números:
>>> @plus5
... @div2
... def prod(a, b):
... return a * b
...
>>> prod(4, 3)
11
>>> ((4 * 3) // 2) + 5
11
Ejercicio
Escriba un decorador llamado fabs() que convierta a su valor absoluto los dos primeros
parámetros de la función que decora y devuelva el resultado de aplicar dicha función a sus
dos argumentos. El valor absoluto de un número se obtiene con la función abs().
A continuación probar el decorador con una función fprod() que devuelva el producto de
dos valores, jugando con números negativos y positivos.
¿Podrías extender el decorador para que tuviera en cuenta un número indeterminado de
argumentos posicionales?
Ejemplo
• Entrada: -3 y 7
• Salida: 21
Funciones recursivas
Veamos ahora un ejemplo más real en el que computar el enésimo término de la Sucesión de
Fibonacci utilizando una función recursiva:
>>> fibonacci(10)
55
>>> fibonacci(20)
6765
Si tratamos de extender el ejemplo anterior de Fibonacci para obtener todos los términos de
la sucesión hasta un límite, pero con la filosofía recursiva, podríamos plantear el uso de una
función generadora:
>>> type(fib)
generator
Ejercicio
Escriba una función recursiva que calcule el factorial de un número:
𝑛! = 𝑛 · (𝑛 − 1) · (𝑛 − 2) · . . . · 1
Ejemplo
• Entrada: 5
• Salida: 120
Cuando una variable se define en el espacio de nombres global podemos hacer uso de ella con
total transparencia dentro del ámbito de las funciones del programa:
>>> language
castellano
>>> catalonia()
language= castellano
En el caso de que asignemos un valor a una variable global dentro de una función, no
estaremos modificando ese valor. Por el contrario, estaremos creando una variable en el
espacio de nombres local:
>>> language
castellano
>>> catalonia()
language= catalan
>>> language
castellano
Python nos permite modificar una variable definida en un espacio de nombres global dentro
de una función. Para ello debemos usar el modificador global:
>>> language
castellano
>>> catalonia()
language= catalan
>>> language
catalan
Advertencia: El uso de global no se considera una buena práctica ya que puede inducir
a confusión y tener efectos colaterales indeseados.
Python proporciona dos funciones para acceder al contenido de los espacios de nombres:
locals() Devuelve un diccionario con los contenidos del espacio de nombres local.
globals() Devuelve un diccionario con los contenidos del espacio de nombres global.
>>> language
castellano
>>> catalonia()
(continué en la próxima página)
>>> globals()
{ __name__ : __main__ ,
__doc__ : Automatically created module for IPython interactive environment ,
__package__ : None,
__loader__ : None,
__spec__ : None,
__builtin__ : <module builtins (built-in)>,
__builtins__ : <module builtins (built-in)>,
_ih : [ ,
"language = castellano ",
"def catalonia():\n language = catalan \n print(f {locals()=} )\n ",
language ,
catalonia() ,
globals() ],
_oh : {3: castellano },
_dh : [ /Users/sdelquin ],
In : [ ,
"language = castellano ",
"def catalonia():\n language = catalan \n print(f {locals()=} )\n ",
language ,
catalonia() ,
globals() ],
Out : {3: castellano },
get_ipython : <bound method InteractiveShell.get_ipython of <IPython.terminal.
˓→interactiveshell.TerminalInteractiveShell object at 0x10e70c2e0>>,
EJERCICIOS DE REPASO
AMPLIAR CONOCIMIENTOS
Hasta ahora hemos estado usando objetos de forma totalmente transparente, casi sin ser
conscientes de ello. Pero, en realidad, todo en Python es un objeto, desde números
a funciones. El lenguaje provee ciertos mecanismos para no tener que usar explícitamente
técnicas de orientación a objetos.
Llegados a este punto, investigaremos en profundidad sobre la creación y manipulación de
clases y objetos, y todas las operaciones que engloban este paradigma.1
La programación orientada a objetos (POO) o en sus siglas inglesas OOP es una manera de
programar que permite llevar al código mecanismos usados con entidades de la vida real.
Sus beneficios son los siguientes:
Encapsulamiento Permite empaquetar el código dentro de una unidad (objeto) donde
se puede determinar el ámbito de actuación.
Abstracción Permite generalizar los tipos de objetos a través de las clases y simplificar
el programa.
Herencia Permite reutilizar código al poder heredar atributos y comportamientos de una
clase a otra.
1
Foto original por Rabie Madaci en Unsplash.
Polimorfismo Permite crear múltiples objetos a partir de una misma pieza flexible de
código.
¿Qué es un objeto?
Un objeto representa una instancia única de alguna entidad a través de los valores de sus
atributos e interactuan con otros objetos (o consigo mismos) a través de sus métodos.
Para crear un objeto primero debemos definir la clase que lo contiene. Podemos pensar en
la clase como el molde con el que crear nuevos objetos de ese tipo.
En el proceso de diseño de una clase hay que tener en cuenta – entre otros – el principio
de responsabilidad única7 , intentando que los atributos y los métodos que contenga estén
enfocados a un objetivo único y bien definido.
Empecemos por crear nuestra primera clase. En este caso vamos a modelar algunos de los
droides de la saga StarWars:
Para ello usaremos la palabra reservada class seguido del nombre de la clase:
7
Principios SOLID
2
Fuente de la imagen: Astro Mech Droids.
Existen multitud de droides en el universo StarWars. Una vez que hemos definido la clase
genérica podemos crear instancias/objetos (droides) concretos:
>>> type(c3po)
__main__.StarWarsDroid
>>> type(r2d2)
__main__.StarWarsDroid
>>> type(bb8)
__main__.StarWarsDroid
Añadiendo atributos
Un atributo no es más que una variable, un nombre al que asignamos un valor, con la
particularidad de vivir dentro de una clase o de un objeto.
Los atributos – por lo general – se suelen asignar durante la creación de un objeto, pero
también es posible añadirlos a posteriori:
>>> golden_droid.name
C-3PO
>>> blue_droid.num_feet
3
3
Guía de estilos PEP8 para convenciones de nombres.
Hemos definido un droide «socio». Veremos a continuación que podemos trabajar con él de
una manera totalmente natural:
>>> type(blue_droid.partner_droid)
__main__.StarWarsDroid
>>> blue_droid.partner_droid.num_feet = 2
Añadiendo métodos
Un método es una función que forma parte de una clase o de un objeto. En su ámbito tiene
acceso a otros métodos y atributos de la clase o del objeto al que pertenece.
La definición de un método (de instancia) es análoga a la de una función ordinaria, pero
incorporando un primer parámetro self que hace referencia a la instancia actual del objeto.
Una de las acciones más sencillas que se pueden hacer sobre un droide es encenderlo o
apagarlo. Vamos a implementar estos dos métodos en nuestra clase:
>>> k2so.switch_on()
Hi! I m a droid. Can I help you?
>>> k2so.switch_off()
Bye! I m going to sleep
Inicialización
Existe un método especial que se ejecuta cuando creamos una instancia de un objeto. Este
método es __init__ y nos permite asignar atributos y realizar operaciones con el objeto en
el momento de su creación. También es ampliamente conocido como el constructor.
Veamos un ejemplo de este método con nuestros droides en el que únicamente guardaremos
el nombre del droide como un atributo del objeto:
8 >>> droid.name
9 BB-8
Ejercicio
Escriba una clase MobilePhone que represente un teléfono móvil.
Atributos:
• manufacturer (cadena de texto)
• screen_size (flotante)
• num_cores (entero)
• apps (lista de cadenas de texto)
• status (0: apagado, 1: encendido)
Métodos:
• __init__(self, manufacturer, screen_size, num_cores)
• power_on(self)
• power_off(self)
• install_app(self, app)
• uninstall_app(self, app)
Crear al menos una instancia (móvil) a partir de la clase creada y «jugar» con los métodos,
visualizando cómo cambian sus atributos.
¿Serías capaz de extender el método install_app() para instalar varias aplicaciones a la
vez?
6.2.3 Atributos
Acceso directo
>>> droid.name
C-3PO
Propiedades
Como hemos visto previamente, los atributos definidos en un objeto son accesibles
públicamente. Esto puede parecer extraño a personas desarrolladoras de otros lenguajes.
En Python existe un cierto «sentido de responsabilidad» a la hora de programar y manejar
este tipo de situaciones.
Una posible solución «pitónica» para la privacidad de los atributos es el uso de propiedades.
La forma más común de aplicar propiedades es mediante el uso de decoradores:
• @property para leer el valor de un atributo.
• @name.setter para escribir el valor de un atributo.
Veamos un ejemplo en el que estamos ofuscando el nombre del droide a través de propiedades:
>>> droid.name
inside the getter
N1-G3L
>>> droid.name
inside the getter
Nigel
>>> droid.hidden_name
Nigel
Valores calculados
Una propiedad también se puede usar para devolver un valor calculado (o computado).
A modo de ejemplo, supongamos que la altura del periscopio de los droides astromecánicos
se calcula siempre como un porcentaje de su altura. Veamos cómo implementarlo:
Consejo: La ventaja de usar valores calculados sobre simples atributos es que el cambio de
valor en un atributo no asegura que actualicemos otro atributo, y además siempre podremos
modificar directamente el valor del atributo, con lo que podríamos obtener efectos colaterales
indeseados.
Ocultando atributos
Python tiene una convención sobre aquellos atributos que queremos hacer «privados» (u
ocultos): comenzar el nombre con doble subguión __
Lo que realmente ocurre tras el telón se conoce como «name mangling» y consiste en
modificar el nombre del atributo incorporado la clase como un prefijo. Sabiendo esto podemos
acceder al valor del atributo supuestamente privado:
>>> droid._Droid__name
BC-44
Atributos de clase
Podemos asignar atributos a las clases y serán heredados por todos los objetos instanciados
de esa clase.
A modo de ejemplo, en un principio, todos los droides están diseñados para que obedezcan
a su dueño. Esto lo conseguiremos a nivel de clase, salvo que ese comportamiento se
sobreescriba:
6.2.4 Métodos
Métodos de instancia
>>> droid.move_up(10)
Moving 10 steps
Métodos de clase
>>> Droid.total_droids()
3 droids built so far!
Métodos estáticos
>>> Droid.get_droids_categories()
[ Messeger , Astromech , Power , Protocol ]
Métodos mágicos
Nivel avanzado
Cuando escribimos hello world * 3 ¿cómo sabe el objeto hello world lo que debe hacer
para multiplicarse con el objeto entero 3? O dicho de otra forma, ¿cuál es la implementación
del operador * para «strings» y enteros? En valores numéricos puede parecer evidente
(siguiendo los operadores matemáticos), pero no es así para otros objetos. La solución que
proporciona Python para estas (y otras) situaciones son los métodos mágicos.
Los métodos mágicos empiezan y terminan por doble subguión __ (es por ello que también
se les conoce como «dunder-methods»). Uno de los «dunder-methods» más famosos es el
constructor de una clase: __init__().
Para el caso de los operadores, existe un método mágico asociado (que podemos personalizar).
Por ejemplo la comparación de dos objetos se realiza con el método __eq__():
Extrapolando esta idea a nuestro universo StarWars, podríamos establecer que dos droides
son iguales si su nombre es igual, independientemente de que tengan distintos números de
serie:
>>> droid1.__eq__(droid2)
True
Veamos un ejemplo en el que «sumamos» dos droides. Esto se podría ver como una fusión.
Supongamos que la suma de dos droides implica: a) que el nombre del droide resultante es
la concatenación de los nombres de los droides; b) que la energía del droide resultante es la
suma de la energía de los droides:
...
__str__
Uno de los métodos mágicos más utilizados es __str__ que permite establecer la forma en
la que un objeto es representado como cadena de texto:
>>> str(droid)
Droid "K-2SO" serial-no 8403898409432
Ejercicio
Defina una clase Fraction que represente una fracción con numerador y denominador enteros
y utilice los métodos mágicos para poder sumar, restar, multiplicar y dividir estas fracciones.
Además de esto, necesitaremos:
• gcd(a, b) como método estático siguiendo el algoritmo de Euclides para calcular
el máximo común divisor entre a y b.
• __init__(self, num, den) para construir una fracción (incluyendo simplificación de
sus términos mediante el método gcd().
• __str__(self) para representar una fracción.
Compruebe que se cumplen las siguientes igualdades:
[︂ ]︂ [︂ ]︂ [︂ ]︂ [︂ ]︂
25 40 31 25 40 −1 25 40 20 25 40 15
+ = − = * = / =
30 45 18 30 45 18 30 45 27 30 45 16
Gestores de contexto
Otra de las aplicaciones de los métodos mágicos (especiales) que puede ser interesante es la
de gestores de contexto. Se trata de un bloque de código en Python que engloba una serie
de acciones a la entrada y a la salida del mismo.
Hay dos métodos que son utilizados para implementar los gestores de contexto:
__enter__() Acciones que se llevan a cabo al entrar al contexto.
__exit__() Acciones que se llevan a cabo al salir del contexto.
Veamos un ejemplo en el que implementamos un gestor de contexto que mide tiempos de
ejecución:
Ahora podemos probar nuestro gestor de contexto con un ejemplo concreto. La forma de
«activar» el contexto es usar la sentencia with seguida del símbolo que lo gestiona:
6.2.5 Herencia
Nivel intermedio
La herencia consiste en crear una nueva clase partiendo de una clase existente, pero
que añade o modifica ciertos aspectos. Se considera una buena práctica tanto para reutilizar
código como para realizar generalizaciones.
6
Iconos por Freepik.
Nota: Cuando se utiliza herencia, la clase derivada, de forma automática, puede usar todo
el código de la clase base sin necesidad de copiar nada explícitamente.
Para que una clase «herede» de otra, basta con indicar la clase base entre paréntesis en la
definición de la clase derivada.
Sigamos con el ejemplo. Una de las grandes categorías de droides en StarWars es la de droides
de protocolo. Vamos a crear una herencia sobre esta idea:
>>> r2d2.switch_off()
Bye! I m going to sleep
Sobreescribir un método
Como hemos visto, una clase derivada hereda todo lo que tiene su clase base. Pero en muchas
ocasiones nos interesa modificar el comportamiento de esta herencia.
En el ejemplo vamos a modificar el comportamiento del método switch_on() para la clase
derivada:
>>> r2d2.switch_on()
Hi! I m a droid. Can I help you?
Añadir un método
La clase derivada también puede añadir métodos que no estaban presentes en su clase base.
En el siguiente ejemplo vamos a añadir un método translate() que permita a los droides
de protocolo traducir cualquier mensaje:
>>> class Droid:
... def switch_on(self):
... print("Hi! I m a droid. Can I help you?")
...
... def switch_off(self):
... print("Bye! I m going to sleep")
...
Con esto ya hemos aportado una personalidad diferente a los droides de protocolo, a pesar
de que heredan de la clase genérica de droides de StarWars.
Puede darse la situación en la que tengamos que acceder desde la clase derivada a métodos
o atributos de la clase base. Python ofrece super() como mecanismo para ello.
Veamos un ejemplo más elaborado con nuestros droides:
>>> class Droid:
... def __init__(self, name):
... self.name = name
(continué en la próxima página)
Herencia múltiple
Nivel avanzado
Aunque no está disponible en todos los lenguajes de programación, Python sí permite que
los objetos pueden heredar de múltiples clases base.
Si en una clase se hace referencia a un método o atributo que no existe, Python lo buscará
en todas sus clases base. Es posible que exista una colisión en caso de que el método o
el atributo buscado esté, a la vez, en varias clases base. En este caso, Python resuelve el
conflicto a través del orden de resolución de métodos4 .
Supongamos que queremos modelar la siguiente estructura de clases con herencia múltiple:
Todas las clases en Python disponen de un método especial llamado mro() que devuelve una
lista de las clases que se visitarían en caso de acceder a un método o un atributo. También
existe el atributo __mro__ como una tupla de esas clases:
>>> SuperDroid.mro()
[__main__.SuperDroid,
__main__.ProtocolDroid,
__main__.AstromechDroid,
__main__.Droid,
object]
>>> HyperDroid.__mro__
(__main__.HyperDroid,
__main__.AstromechDroid,
__main__.ProtocolDroid,
__main__.Droid,
object)
>>> super_droid.greet()
Here a protocol droid
>>> hyper_droid.greet()
Here an astromech droid
Nota: Todos los objetos en Python heredan, en primera instancia, de object. Esto se puede
comprobar con el mro() correspondiente:
>>> int.mro()
[int, object]
(continué en la próxima página)
>>> str.mro()
[str, object]
>>> float.mro()
[float, object]
>>> tuple.mro()
[tuple, object]
>>> list.mro()
[list, object]
>>> bool.mro()
[bool, int, object]
Mixins
Hay situaciones en la que nos interesa incorporar una clase base «independiente» de la
jerarquía establecida, y sólo a efectos de tareas auxiliares. Esta aproximación podría ayudar
a evitar colisiones en métodos o atributos reduciendo la ambigüedad que añade la herencia
múltiple. Estas clases auxiliares reciben el nombre de «mixins».
Veamos un ejemplo en el que usamos un «mixin» para mostrar las variables de un objeto:
>>> droid.dig()
{ code : DN-LD , num_feet : 2, type : Power Droid }
Ejercicio
Se pide lo siguiente:
1. Cree las 3 clases de la imagen anterior con la herencia señalada.
2. Cree un objeto de tipo VideoFile con las siguientes características:
• path: /home/python/vanrossum.mp4
• codec: h264
• geoloc: (23.5454, 31.4343)
• duration: 487
• dimensions: (1920, 1080)
3. Añada el contenido audio/ogg al fichero.
4. Añada el contenido video/webm al fichero.
5. Imprima por pantalla la info() de este objeto (el método info() debería retornar str
y debería hacer uso de los métodos info() de las clases base).
Salida esperada:
� El método size() debe devolver el número total de caracteres sumando las longitudes de
los elementos del atributo contents.
Agregación y composición
Aunque la herencia de clases nos permite modelar una gran cantidad de casos de uso en
términos de «is-a» (es un), existen muchas otras situaciones en las que la agregación o
la composición son una mejor opción. En este caso una clase se compone de otras cases:
hablamos de una relación «has-a» (tiene un).
Hay una sutil diferencia entre agregación y composición:
• La composición implica que el objeto utilizado no puede «funcionar» sin la presencia
de su propietario.
• La agregación implica que el objeto utilizado puede funcionar por sí mismo.
>>> print(bb8)
Droid BB-8 armed with a LIGHTER
EJERCICIOS DE REPASO
1. Escriba una clase en Python para representar una secuencia de ADN. De momento, la
clase sólo contendrá los siguientes elementos:
• 4 atributos de clase, cada uno representando una base nitrogenada con su valor
como un carácter.
• Constructor que recibe una secuencia de caracteres (bases).
• Método para representar el objeto en formato «string».
2. Continúe con el ejercicio anterior, y añada a la clase 4 propiedades que calculen el
número total de cada una de las bases presentes en la secuencia.
3. Continúe con el ejercicio anterior, y añada a la clase un método de instancia para sumar
dos secuencias de ADN. La suma se hará base a base y el resultado será el máximo de
cada letra(base).
4. Continúe con el ejercicio anterior, y añada a la clase un método de instancia para
obtener el porcentaje de aparición de cada base (usando las propiedades definidas en
ejercicios anteriores).
5. Continúe con el ejercicio anterior, y añada a la clase un método de instancia para
multiplicar dos secuencias de ADN. La multiplicación consiste en dar como salida una
nueva secuencia que contenga sólo aquellas bases que coincidan en posición en ambas
secuencias de entrada.
AMPLIAR CONOCIMIENTOS
6.3 Excepciones
Si una excepción ocurre en algún lugar de nuestro programa y no es capturada en ese punto,
va subiendo (burbujeando) hasta que es capturada en alguna función que ha hecho la llamada.
Si en toda la «pila» de llamadas no existe un control de la excepción, Python muestra un
mensaje de error con información adicional:
>>> intdiv(3, 0)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 2, in intdiv
ZeroDivisionError: integer division or modulo by zero
Para manejar (capturar) las excepciones podemos usar un bloque de código con las palabras
reservadas try and except:
>>> intdiv(3, 0)
Please do not divide by zero...
Aquel código que se encuentre dentro del bloque try se ejecutará normalmente siempre y
cuando no haya un error. Si se produce una excepción, ésta será capturada por el bloque
except, ejecutándose el código que contiene.
Consejo: No es una buena práctica usar un bloque except sin indicar el tipo de excepción
que estamos gestionando, no sólo porque puedan existir varias excepciones que capturar sino
1
Foto original por Sarah Kilian en Unsplash.
Especificando excepciones
>>> intdiv(3, 0)
Please do not divide by zero...
>>> intdiv(3, 0 )
Check operands. Some of them seems strange...
Python proporciona la cláusula else para saber que todo ha ido bien y que no se ha lanzado
ninguna excepción. Esto es relevante a la hora de manejar los errores.
De igual modo, tenemos a nuestra disposición la cláusula finally que se ejecuta siempre,
independientemente de si ha habido o no ha habido error.
Veamos un ejemplo de ambos:
>>> user_index = 3
>>> try:
... r = values[user_index]
... except IndexError:
... print( Error: Index not in list )
... else:
... print(f Your wishes are my command: {r} )
... finally:
... print( Have a good day! )
...
Error: Index not in list
Have a good day!
>>> user_index = 2
>>> try:
... r = values[user_index]
... except IndexError:
... print( Error: Index not in list )
... else:
... print(f Your wishes are my command: {r} )
... finally:
... print( Have a good day! )
...
Your wishes are my command: 7
Have a good day!
Ejercicio
Sabiendo que ValueError es la excepción que se lanza cuando no podemos convertir una
cadena de texto en su valor numérico, escriba una función get_int() que lea un valor entero
del usuario y lo devuelva, iterando mientras el valor no sea correcto.
Ejecución a modo de ejemplo:
Nivel avanzado
Python ofrece una gran cantidad de excepciones predefinidas. Hasta ahora hemos visto
cómo gestionar y manejar este tipo de excepciones. Pero hay ocasiones en las que nos
puede interesar crear nuestras propias excepciones. Para ello tendremos que crear una clase
heredando de Exception, la clase base para todas las excepciones.
Veamos un ejemplo en el que creamos una excepción propia controlando que el valor sea un
número entero:
Hemos usado la sentencia raise para «elevar» esta excepción, que podría ser controlada en
un nivel superior mediante un bloque try - except.
Nota: Para crear una excepción propia basta con crear una clase vacía. No es necesario
incluir código más allá de un pass.
Mensaje personalizado
Podemos ir un paso más allá e incorporar en el mensaje el propio valor que está generando
el error:
6.3.3 Aserciones
Si hablamos de control de errores hay que citar una sentencia en Python denominada assert.
Esta sentencia nos permite comprobar si se están cumpliendo las «expectativas» de nuestro
programa, y en caso contrario, lanza una excepción informativa.
Su sintaxis es muy simple. Únicamente tendremos que indicar una expresión de comparación
después de la sentencia:
>>> result = 10
>>> print(result)
10
En el caso de que la condición se cumpla, no sucede nada: el programa continúa con su flujo
normal. Esto es indicativo de que las expectativas que teníamos se han satisfecho.
Sin embargo, si la condición que fijamos no se cumpla, la aserción devuelve un error
AssertionError y el programa interrupme su ejecución:
>>> result = -1
AssertionError:
Podemos observar que la excepción que se lanza no contiene ningún mensaje informativo. Es
posible personalizar este mensaje añadiendo un segundo elemento en la tupla de la aserción:
AMPLIAR CONOCIMIENTOS
6.4 Módulos
Escribir pequeños trozos de código puede resultar interesante para realizar determinadas
pruebas. Pero a la larga, nuestros programas tenderán a crecer y será necesario agrupar el
código en unidades manejables.
Los módulos son simplemente ficheros de texto que contienen código Python y representan
unidades con las que evitar la repetición y favorecer la reutilización.1
Para hacer uso del código de otros módulos usaremos la sentencia import. Esto permite
importar el código y las variables de dicho módulo para que estén disponibles en nuestro
programa.
La forma más sencilla de importar un módulo es import <module> donde module es el nombre
de otro fichero Python, sin la extensión .py.
Supongamos que partimos del siguiente fichero (módulo):
arith.py
10
15
Desde otro fichero - en principio en la misma carpeta - podríamos hacer uso de las funciones
definidas en arith.py.
Desde otro fichero haríamos lo siguiente para importar todo el contenido del módulo arith.
py:
3 >>> arith.addere(3, 7)
4 10
>>> sys.path
[ /path/to/.pyenv/versions/3.9.1/envs/aprendepython/bin ,
/path/to/.pyenv/versions/3.9.1/lib/python3.9 ,
/path/to/.pyenv/versions/3.9.1/envs/aprendepython/lib/python3.9/site-packages ,
]
La cadena vacía que existe al final de la lista hace referencia a la carpeta actual.
$ export PYTHONPATH=/tmp
>>> sys.path
[ /path/to/.pyenv/versions/3.9.1/envs/aprendepython/bin ,
/tmp ,
/path/to/.pyenv/versions/3.9.1/lib/python3.9 ,
/path/to/.pyenv/versions/3.9.1/envs/aprendepython/lib/python3.9/site-packages ,
]
Modificando directamente la lista sys.path Para ello accedemos a lista que está en el
módulo sys de la librería estandar:
>>> sys.path
[ /path/to/.pyenv/versions/3.9.1/envs/aprendepython/bin ,
/path/to/.pyenv/versions/3.9.1/lib/python3.9 ,
/path/to/.pyenv/versions/3.9.1/envs/aprendepython/lib/python3.9/site-packages ,
,
/tmp ]
>>> sys.path
[ /tmp ,
/path/to/.pyenv/versions/3.9.1/envs/aprendepython/bin ,
(continué en la próxima página)
Es posible que no necesitemos todo aquello que está definido en arith.py. Supongamos que
sólo vamos a realizar divisiones. Para ello haremos lo siguiente:
3 >>> partitus(5, 2)
4 2.5
Nota: Nótese que en la línea 3 ya podemos hacer uso directamente de la función partitus()
porque la hemos importado directamente. Este esquema tiene el inconveniente de la posible
colisión de nombres, en aquellos casos en los que tuviéramos algún objeto con el mismo
nombre que el objeto que estamos importando.
Hay ocasiones en las que interesa, por colisión de otros nombres o por mejorar la legibilidad,
usar un nombre diferente del módulo (u objeto) que estamos importando. Python nos ofrece
esta posibilidad a través de la sentencia as.
Supongamos que queremos importar la función del ejemplo anterior pero con otro nombre:
>>> mydivision(5, 2)
2.5
6.4.2 Paquetes
Un paquete es simplemente una carpeta que contiene ficheros .py. Además permite tener
una jerarquía con más de un nivel de subcarpetas anidadas.
Para ejemplificar este modelo vamos a crear un paquete llamado mymath que contendrá 2
módulos:
• arith.py para operaciones aritméticas (ya visto anteriormente).
• logic.py para operaciones lógicas.
El código del módulo de operaciones lógicas es el siguiente:
logic.py
10
1 .
2 ├── main.py
3 └── mymath
4 ├── arith.py
5 └── logic.py
6
7 1 directory, 3 files
Si ya estamos en el fichero main.py (o a ese nivel) podremos hacer uso de nuestro paquete
de la siguiente forma:
3 >>> arith.pullulate(4, 7)
4 28
5
6 >>> logic.et(1, 0)
7 0
Consejo: Suele ser una buena práctica llamar main.py al fichero que contiene nuestro
programa principal.
# CÓDIGO PROPIO
# ...
# CÓDIGO PROPIO
if __name__ == __main__ :
# punto de entrada real
$ python3 main.py
if __name__ == __main__
Esta condición permite, en el programa principal, diferenciar qué codigo se lanzará cuando
el fichero se ejecuta directamente o cuando el fichero se importa desde otro lugar.
hello.py
1 import blabla
2
4 def myfunc():
5 print( Inside myfunc )
6 blabla.hi()
7
9 if __name__ == __main__ :
10 print( Entry point )
11 myfunc()
AMPLIAR CONOCIMIENTOS
Procesamiento de texto
Además de las herramientas que se han visto en cadenas de texto, la librería estándar nos
ofrece una serie de módulos para procesamiento de texto que nos harán la vida más fácil a
la hora de gestionar este tipo de datos.
269
Aprende Python
7.1 string
El módulo string proporciona una serie de constantes muy útiles para manejo de «strings»,
además de distintas estrategias de formateado de cadenas.1
7.1.1 Constantes
>>> string.ascii_lowercase
abcdefghijklmnopqrstuvwxyz
>>> string.ascii_uppercase
ABCDEFGHIJKLMNOPQRSTUVWXYZ
>>> string.ascii_letters
abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ
>>> string.digits
0123456789
>>> string.octdigits
01234567
>>> string.punctuation
!"#$%&\ ()*+,-./:;<=>?@[\\]^_ {|}~
>>> string.printable
0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!"#$%&\ ()*+,-./:;<=>?
˓→@[\\]^_ {|}~ \t\n\r\x0b\x0c
>>> string.whitespace
\t\n\r\x0b\x0c
Ejercicio
Dada una cadena de texto, compruebe si todos sus caracteres son dígitos ASCII. Ignore los
espacios en blanco.
Ejemplo
• Entrada: This is it
• Salida: True
7.1.2 Plantillas
El módulo string también nos permite usar plantillas con interpolación de variables. Algo
similar a los f-strings pero con otro tipo de sintaxis.
Lo primero es definir la plantilla. Las variables que queramos interporlar deben ir precedidas
del signo dólar $:
from string import Template
Ahora podemos realizar la sustitución con los valores que nos interesen:
>>> tmpl.substitute(lang= Python , place= World )
Python is the best programming language in the World!
Hay que prestar atención cuando el identificador de variable está seguido por algún carácter
que, a su vez, puede formar parte del identificador. En este caso hay que utilizar llaves para
evitar la ambigüedad:
Sustitución segura
En el caso de que alguna de las variables que estamos interpolando no exista o no tenga
ningún valor, obtendremos un error al sustituir:
>>> tmpl = Template( $lang is the best programming language in the $place! )
Para ello Python nos ofrece el método safe_substitute() que no emite error si alguna
variable no es especificada:
Casos de uso
A primera vista podría parecer que este sistema de plantillas no aporta gran ventaja sobre
los f-strings que ya hemos visto. Sin embargo hay ocasiones en los que puede resultar muy
útil.
La mayoría de estas situaciones tienen que ver con la oportunidad de definir el «string».
Si en el momento de crear la plantilla aún no están disponibles las variables de sustitución,
podría interesar utilizar la estrategia que nos proporciona este módulo.
Supongamos un ejemplo en el que tenemos una estructura de «url» y queremos únicamente
sustituir una parte de ella. Para no tener que repetir la cadena de texto completa en un
«f-string», podríamos seguir este enfoque:
Ciencia de datos
La Ciencia de Datos representa uno de los ámbitos de aplicación más importantes dentro
del mundo Python. De hecho, en la encuesta a desarrolladores/as de JetBrains del año 2021
se puede observar que análisis de datos y aprendizaje automático están en primera y
cuarta opción en la pregunta ¿para qué utiliza Python?.
Muchos de los paquetes que veremos en esta sección también vienen incluidos por defecto
en Anaconda una plataforma para desarrollo de ciencia de datos que dispone de instaladores
para todos los sistemas operativos.
275
Aprende Python
8.1 jupyter
El módulo jupyter proporciona un entorno de desarrollo integrado para ciencia de datos, que
no es exclusivo de Python, sino que además admite otros lenguajes en su «backend».1
$ jupyter notebook
Nota: Este comando nos debería abrir una ventana en el navegador web por defecto del
sistema, apuntando a la dirección http://localhost:8888
1
Foto original de portada por NASA en Unsplash.
2
Un «notebook» es el concepto de cuaderno (documento) científico que se maneja en Jupyter
8.1.1 Notebooks
Un «notebook» es un documento que está compuesto por celdas en las que podemos incluir:
• Texto en formato markdown (incluyendo fórmulas).
• Elementos multimedia.
• Código Python ejecutable.
Nota: Los «notebooks» o cuadernos son básicamente archivos de texto en formato json con
extensión .ipynb (que proviene de «IPython Notebook»).
8.1.2 Interfaz
Jupyter se presenta como una aplicación web en cuya interfaz podemos encontrar distintos
elementos que nos permitirán desarrollar nuestras tareas de programación de una forma más
cómoda.
Explorador de archivos
Nota: Los «notebooks» que se están ejecutando suelen tener un color verde en el icono,
mientras que los que están parados aparecen en gris.
Barra de menú
Menú Fichero
Del estilo de los menús tradicionales de aplicaciones, aquí podemos encontrar las principales
funciones sobre ficheros.
Checkpoints: Permiten guardar el estado del «notebook» en un momento determinado
para luego poder revertirlo a ese momento del tiempo.
Exportar notebooks: Es posible exportar «notebooks» a una gran variedad de formatos:
• Python (.py)
• HTML (.html)
• Reveal.js «slides» (.html)
• Markdown (.md)
• reST (.rst)
• PDF vía LaTeX (.pdf)
• asciidoc (.asciidoc)
• custom (.txt)
• LaTeX (.tex)
Ejercicio
Cree un «notebook» de prueba y descárgelo en formato HTML y Markdown.
Menú Edición
Este menú contiene las acciones que podemos realizar sobre una o varias celdas.
Las funciones las podríamos agrupar en gestión de celdas (cortar, pegar, borrar, dividir,
unir, mover, etc.) e inserción de imágenes seleccionando desde un cuadro de diálogo.
Menú Vista
Menú Insertar
Menú Celda
• Code: para incluir código (se podrá ejecutar el lenguaje de programación según
el «kernel» instalado).
• Markdown: para escribir texto utilizando sintaxis markdown.
• Raw: estas celdas no serán formateadas.
Salida de celdas: La ejecución de las celdas de código tiene (suele tener) una salida. Esta
salida se puede ocultar (si interesa). Incluso tenemos control sobre activar o desactivar
el «scroll» en caso de que la salida sea muy larga.
Menú Kernel
El kernel es la capa de software que se encarga de ejecutar las celdas de nuestro «notebook»
que contienen código. Podemos tener instalados distintos «kernels» para un mismo Jupyter
Notebook. El kernel se puede interrumpir o reiniciar.
Hay veces, que debido a un error de programación o a procesos muy largos, podemos
encontrarnos con el «kernel» bloqueado durante un largo período de tiempo. En estas
Menú Ayuda
Como cualquier aplicación, existe un menú de ayuda en el que se pueden encontrar enlaces
a referencias y manuales.
Uno de los elementos más interesantes de la ayuda es el uso de los «shortcuts»3 . Aunque hay
muchos, dejamos aquí algunos de los más útiles:
Shortcut Acción
SHIFT + ENTER Ejecutar la celda actual
ALT + ENTER Ejecutar la celda actual y «abrir» una celda debajo
a Abrir una celda encima de la actual («above»)
b Abrir una celda debajo de la actual («below»)
m Convertir la celda actual a Markdown
y Convertir la celda actual a código
dd Borrar la celda actual
8.1.3 MathJax
3
Un «shortcut» es un «atajo de teclado» (combinación de teclas) para lanzar una determinada acción.
Fórmulas «de bloque»: Se debe usar el delimitador doble dólar antes y después de la
expresión $$ ... $$
Por ejemplo: $$ \sum_{x=1}^n sin(x) + cos(x) $$ produce:
𝑛
∑︁
𝑠𝑖𝑛(𝑥) + 𝑐𝑜𝑠(𝑥)
𝑥=1
Ejemplos de fórmulas
\dot{x} = \sigma(y-x) \\
\dot{y} = \rho x - y - xz \\
\dot{z} = -\beta z + xy
𝑥˙ = 𝜎(𝑦 − 𝑥)
𝑦˙ = 𝜌𝑥 − 𝑦 − 𝑥𝑧
𝑧˙ = −𝛽𝑧 + 𝑥𝑦
Ecuaciones en varias líneas (con alineación):
\begin{align}
\dot{x} &= \sigma(y-x) \\
\dot{y} &= \rho x - y - xz \\
\dot{z} &= -\beta z + xy
\end{align}
𝑥˙ = 𝜎(𝑦 − 𝑥)
𝑦˙ = 𝜌𝑥 − 𝑦 − 𝑥𝑧
𝑧˙ = −𝛽𝑧 + 𝑥𝑦
Usando paréntesis:
⃒ i j k⃒⃒
⃒ ⃒
V1 × V2 = ⃒⃒ 𝜕𝑋
⃒ 𝜕𝑌
𝜕𝑢 𝜕𝑢
0⃒⃒
⃒ 𝜕𝑋 𝜕𝑌
0⃒
𝜕𝑣 𝜕𝑣
Algo de probabilidad:
1 𝑒−2𝜋
(︁√︀ √ )︁ 2 = 1 + −4𝜋
𝜑 5 − 𝜑 𝑒5𝜋 1 + 𝑒 𝑒−6𝜋
1+ −8𝜋
1+ 𝑒1+...
1 + \frac{q^2}{(1-q)}+\frac{q^6}{(1-q)(1-q^2)}+\cdots =
\prod_{j=0}^{\infty}\frac{1}{(1-q^{5j+2})(1-q^{5j+3})},
\quad\quad \text{for $|q|<1$}.
∞
𝑞2 𝑞6 1
for |𝑞| < 1.
∏︁
1+ + 2
+ · · · = 5𝑗+2 5𝑗+3 )
,
(1 − 𝑞) (1 − 𝑞)(1 − 𝑞 ) 𝑗=0
(1 − 𝑞 )(1 − 𝑞
\begin{eqnarray}
x &=& &x \sin\phi &+& z \cos\phi \\
z &=& - &x \cos\phi &+& z \sin\phi
\end{eqnarray}
𝑥′ = 𝑥 sin 𝜑 + 𝑧 cos 𝜑
𝑧 = − 𝑥 cos 𝜑 + 𝑧 sin 𝜑
′
Ejercicio
Escriba en MathJax las siguientes ecuaciones:
Ecuación 1
∫︁ 𝑏
𝑓 ′ (𝑥)𝑑𝑥 = 𝑓 (𝑏) − 𝑓 (𝑎)
𝑎
Ecuación 2
1
𝑡′ = 𝑡 √︁
𝑣2
1− 𝑐2
Ecuación 3
[︁ 𝜕 𝜕 ]︁
𝑀 + 𝛽(𝑔) + 𝜂𝛾 𝐺𝑛 (𝑥1 , 𝑥2 , . . . , 𝑥𝑛 ; 𝑀, 𝑔) = 0
𝜕𝑀 𝜕𝑔
Ecuación 4
1 ∑︁ 𝜕 2 ℎ00 4𝜋𝐺
𝑅00 ≈ − 𝑖 2
= 2 (𝜌𝑐2 ) ⇒ ▽2 𝜑𝑔 = 4𝜋𝐺𝜌
2 𝑖 𝜕(𝑥 ) 𝑐
Truco: Puede encontrar símbolos matemáticos para Latex en este enlace así como dibujar
directamente un símbolo y obtener su referencia a través de la herramienta Detexify.
Jupyter Notebook ofrece una gama de comandos especiales que cubren gran variedad de
funcionalidades.
Comandos de shell
>>> !date
martes, 15 de junio de 2021, 09:13:25 WEST
>>> !whoami
sdelquin
Ejercicio
Ejecute los siguientes comandos del sistema y obtenga la salida en una celda del Notebook:
Obteniendo ayuda
Una de las formas más sencillas de obtener información de librerías, funciones o módulos es
utilizar el sufijo interrogación ?
>>> import random
>>> random.randint?
Signature: random.randint(a, b)
Docstring:
Return random integer in range [a, b], including both end points.
File: ~/.pyenv/versions/3.9.1/lib/python3.9/random.py
Type: method
Ejercicio
Obtenga la documentación de las siguientes funciones:
• os.path.dirname
• re.match
• datetime.timedelta
Comandos mágicos
Representando gráficas
Otra de las grandes ventajas que ofrece Jupyter Notebook es poder graficar directamente
sobre el cuaderno. Para ello utilizamos código Python (en este caso) y una directiva de
comando mágico para indicar que se renderice en línea:
>>> plt.plot(x, y)
[<matplotlib.lines.Line2D at 0x106414e50>]
<Figure size 432x288 with 1 Axes>
Manejando ficheros
Cargando un fichero en la celda actual: Para ello utilizamos el comando %load "ruta/
al/fichero"
Ejecutando un fichero en la celda actual: Para ello utilizamos el comando %run
"ruta/al/fichero"
Escribiendo el contenido de la celda actual a fichero: Para ello utilizamos el
comando %writefile "ruta/al/fichero" como primera línea de la celda y
después vendría el código que queremos escribir.
Ejercicio
• En una celda del «notebook», escriba código Python para crear una lista de 100
números pares.
• Guarde el contenido de esa celda un fichero Python usando %%writefile
• Carge este fichero en una celda con %load
• Ejecútelo con %run
Tiempos de ejecución
Para medir el tiempo de ejecución de una determinada instrucción Python podemos utilizar
el comando %timeit que calcula un promedio tras correr repetidas veces el código indicado:
De igual forma, existe un mecanismo para medir el tiempo de ejecución de una celda
completa. En este caso se utiliza el comando %%timeit (nótese la diferencia del doble
porcentaje como prefijo):
%%timeit
numpy.random.poisson(size=100)
numpy.random.uniform(size=100)
numpy.random.logistic(size=100)
8.88 µs ± 25.8 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)
Ejercicio
Mida si hay diferencias significativas en tiempos de ejecución en la creación de distribuciones
aleatorias atendiendo a:
• Tipo de distribución (Poisson, Uniform, Logistic).
• Tamaño de la muestra (100, 10000, 1000000).
Celdas con HTML: Si necesitamos insertar código HTML en una celda, podemos usar el
comando %%html al comienzo de la misma:
%%html
<iframe src="https://www.google.com/maps/embed?pb=!1m18!1m12!1m3!1d3592984.
˓→8538165656!2d-18.096789575396794!3d28.426067294993228!2m3!1f0!2f0!3f0!3m2!
˓→1i1024!2i768!4f13.1!3m3!1m2!1s0xc41aa86ef755363%3A0x10340f3be4bc8c0!
˓→2sCanarias!5e0!3m2!1ses!2ses!4v1623755509663!5m2!1ses!2ses" width="400"␣
Celdas con «shell script»: Hay ocasiones en las que un código en shell script suele ser
útil. Para incluirlo recurrimos al comando %%bash al principio de la celda:
%%bash
!tree -d -L 2
.
├── __pycache__
├── _build
│ └── html
├── _static
│ ├── css
│ ├── img
│ └── js
├── core
│ ├── controlflow
│ ├── datastructures
│ ├── datatypes
│ ├── devenv
│ ├── introduction
│ └── modularity
├── miniprojects
(continué en la próxima página)
20 directories
Celdas con perl: No hay que subestimar el poder del lenguaje de programación perl. Si
fuera necesario, lo podemos incluir en una celda del «notebook» con %%perl al comienzo
de la misma:
%%perl
my $email = sdelquin@gmail.com ;
if ($email =~ /^([^@]+)\@(.+)$/) {
print "Username is: $1\n";
print "Hostname is: $2\n";
}
...
8.1.5 Extensiones
El ecosistema de entornos para trabajos en ciencia de datos ha ido ampliándose durante estos
últimos años con la explosión del «BigData» y la inteligencia artificial. En este apartado
veremos otras plataformas que también nos permiten usar Python enfocado al análisis de
datos.
JupyterLab
JupyterLab es una evolución de Jupyter Notebook. Entre sus mejoras podemos destacar:
• Explorador de ficheros integrado en la barra lateral.
• Posibilidad de abrir múltiples .ipynb al mismo tiempo usando pestañas.
• Posibilidad de abrir múltiples terminales.
• Editor integrado para cualquier fichero de texto.
• Vista previa en tiempo real de documentos markdown o csv.
$ jupyter-lab
Google Colab
Características:
• Tiene un comportamiento totalmente análogo a Jupyter en cuanto a comportamiento
y funcionalidades.
• Completamente en la nube. No necesita instalación ni configuración.
• Por defecto trae multitud de paquetes instalados, principalmente en el ámbito científico:
386 paquetes (febrero de 2022).
• Versión de Python: 3.7.12 (febrero de 2022).
• Espacio en disco sujeto a las características de Google Compute Engine: 107.72GB
(febrero de 2022)
• Memoria RAM sujeta a las características de Google Compute Engine: 12.69GB
(febrero de 2022)
• Acceso limitado al sistema operativo.
• En cuentas gratuitas, los tiempos de cómputo son, por lo general, mayores que en una
máquina local.4
• Previsualización markdown en tiempo real sobre cada celda.
• Posibilidad de subir ficheros de datos propios en carpetas accesibles por el cuaderno.
• Posibilidad de ejecutar Jupyter «notebooks» propios.
• Posibilidad (limitada) de acelerar cálculos usando GPU6 o TPU7 .
• Posibilidad de descargar el cuaderno como Jupyter «notebook» o archivo de Python.
• Índice de contenidos integrado en barra lateral.
• Inspector de variables integrado en barra lateral.
Kaggle
Kaggle es una plataforma que no sólo ofrece un entorno de trabajo para cuadernos Jupyter
sino también una enorme colección de conjuntos de datos de libre acceso. Para su uso es
necesario disponer de una cuenta en el servicio.
Características:
4
Todo estará en función de las características de la máquina con la que se esté trabajando.
6
Graphics Processing Unit (Unidad gráfica de procesamiento).
7
Tensor Processing Unit (Unidad de procesamiento tensorial).
Comparativa
Haremos una comparativa de tiempos de ejecución lanzando una FFT5 sobre una matriz de
1 millón de elementos:
8.2 numpy
8.2.1 ndarray
1
Foto original de portada por Vlado Paunovic en Unsplash.
>>> x
array([1, 2, 3, 4, 5])
>>> type(x)
numpy.ndarray
Si queremos obtener información sobre el array creado, podemos acceder a distintos atributos
del mismo:
Datos heterogéneos
Hemos dicho que los ndarray son estructuras de datos que almacenan un único tipo de datos.
A pesar de esto, es posible crear un array con los siguientes valores:
Aunque, a priori, puede parecer que estamos mezclando tipos enteros, flotantes y cadenas de
texto, lo que realmente se produce (de forma implícita) es una coerción2 de tipos a Unicode:
>>> x
array([ 4 , Einstein , 1e-07 ], dtype= <U32 )
>>> x.dtype
dtype( <U32 )
2
Característica de los lenguajes de programación que permite, implícita o explícitamente, convertir un
elemento de un tipo de datos en otro, sin tener en cuenta la comprobación de tipos.
Tipos de datos
NumPy maneja gran cantidad de tipos de datos. A diferencia de los tipos de datos numéricos
en Python que no establecen un tamaño de bytes de almacenamiento, aquí sí hay una
diferencia clara.
Algunos de los tipos de datos numéricos en NumPy se presentan en la siguiente tabla:
Truco: NumPy entiende por defecto que int hace referencia a np.int64 y que float hace
referencia a np.float64. Son «alias» bastante utilizados.
Si creamos un array de números enteros, el tipo de datos por defecto será int64:
>>> a = np.array(range(10))
>>> a
array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
>>> a.dtype
dtype( int64 )
>>> b
array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9], dtype=int32)
>>> b.dtype
dtype( int32 )
Lo mismo ocurre con valores flotantes, donde float64 es el tipo de datos por defecto.
Es posible convertir el tipo de datos que almacena un array mediante el método astype. Por
ejemplo:
>>> a
array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
>>> c = a.astype(float)
>>> c.dtype
dtype( float64 )
ndarray vs list
Nota: El cómputo es casi 12 veces más rápido utilizando ndarray frente a listas clásicas.
>>> a
array([10, 20, 30])
>>> b = a.tolist()
>>> b
[10, 20, 30]
>>> type(b)
list
Matrices
Una matriz no es más que un array bidimensional. Como ya se ha comentado, NumPy provee
ndarray que se comporta como un array multidimensional con lo que podríamos crear una
matriz sin mayor problema.
Veamos un ejemplo en el que tratamos de construir la siguiente matriz:
⎡ ⎤
1 2 3
⎢4 5 6⎥
𝑀 =⎢ ⎣7 8 9⎦
⎥
10 11 12
>>> M = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9], [10, 11, 12]])
>>> M
array([[ 1, 2, 3],
[ 4, 5, 6],
[ 7, 8, 9],
[10, 11, 12]])
>>> M.size
12
>>> M.dtype
dtype( int64 )
Ejercicio
Cree los siguientes arrays en NumPy:
array1 = 88 23 39 41
[︀ ]︀
[︂ ]︂
76.4 21.7 38.4
array2 =
41.2 52.8 68.9
⎡ ⎤
12
⎢4⎥
array3 = ⎢
⎣9⎦
⎥
Obtenga igualmente las siguientes características de cada uno de ellos: dimensión, tamaño,
forma y tipo de sus elementos.
Cambiando la forma
Advertencia: En el caso de que no exista posibilidad de cambiar la forma del array por
el número de filas y/o columnas especificado, obtendremos un error de tipo ValueError:
cannot reshape array.
Almacenando arrays
Es posible que nos interese almacenar (de forma persistente) los arrays que hemos ido
creando. Para ello NumPy nos provee, al menos, de dos mecanismos:
Almacenamiento en formato binario propio: Mediante el método save() podemos
guardar la estructura de datos en ficheros .npy. Veamos un ejemplo:
>>> M
array([[ 1, 2, 3],
[ 4, 5, 6],
[ 7, 8, 9],
[10, 11, 12]])
>>> M_reloaded
array([[ 1, 2, 3],
[ 4, 5, 6],
[ 7, 8, 9],
[10, 11, 12]])
>>> M
array([[ 1, 2, 3],
[ 4, 5, 6],
[ 7, 8, 9],
[10, 11, 12]])
>>> col1
array([ 1, 4, 7, 10])
>>> col2
array([ 2, 5, 8, 11])
>>> col3
array([ 3, 6, 9, 12])
NumPy ofrece una gran variedad de funciones predefinidas para creación de arrays que nos
permiten simplificar el proceso de construcción de este tipo de estructuras de datos.
Valores fijos
A continuación veremos una serie de funciones para crear arrays con valores fijos.
Ceros
Por defecto, ésta y otras funciones del estilo, devuelven valores flotantes. Si quisiéramos
trabajar con valores enteros basta con usar el parámetro dtype:
Existe la posibilidad de crear un array de ceros con las mismas dimensiones (y forma)
que otro array:
>>> M
array([[1, 2, 3],
[4, 5, 6]])
>>> np.zeros_like(M)
array([[0, 0, 0],
[0, 0, 0]])
Lo cual sería equivalente a pasar la «forma» del array a la función predefinida de creación
de ceros:
Unos
Mismo valor
Matriz identidad
>>> np.eye(5)
array([[1., 0., 0., 0., 0.],
[0., 1., 0., 0., 0.],
[0., 0., 1., 0., 0.],
[0., 0., 0., 1., 0.],
[0., 0., 0., 0., 1.]])
Matriz diagonal
Ejercicio
Cree la siguiente matriz mediante código Python:
⎡ ⎤
0 0 0 ... 0
⎢0 1 0 ... 0⎥
⎢ ⎥
diagonal = ⎢0 0 2 ... 0⎥
⎢
⎢ .. .. ..
⎥
⎣. . 0 .
⎥
0⎦
0 0 0 . . . 49
Obtenga igualmente las siguientes características de cada uno de ellos: dimensión, tamaño,
forma y tipo de sus elementos.
Valores equiespaciados
A continuación veremos una serie de funciones para crear arrays con valores equiespaciados
o en intervalos definidos.
>>> np.arange(21)
array([ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16,
17, 18, 19, 20])
Valores aleatorios
A continuación veremos una serie de funciones para crear arrays con valores aleatorios y
distribuciones de probabilidad.
>>> np.random.random(9)
array([0.53836208, 0.78315275, 0.6931254 , 0.97194325, 0.01523289,
0.47692141, 0.27653964, 0.82297655, 0.70502383])
Distribuciones de probabilidad
>>> dist[:20]
array([ 2.5290643 , 1.46577658, 1.65170437, -1.36970819, -2.24547757,
7.19905613, -4.4666239 , -1.05505116, 2.42351298, -4.45314272,
1.13604077, -2.85054948, 4.34589478, -2.81235743, -0.8215143 ,
0.57796411, -2.56594122, -7.14899388, 3.49197644, 1.80691996])
>>> dist.mean()
0.004992046432131982
>>> dist.std()
4.998583810032169
>>> coins
array([ tail , head , tail , ..., tail , head , tail ], dtype= <U4 )
>>> dices
array([6, 5, 4, ..., 1, 6, 1])
>>> sum(dices == 1)
500290
>>> sum(dices == 6)
99550
Ver también:
Listado de distribuciones aleatorias que se pueden utilizar en NumPy.
Ejercicio
Cree:
• Una matriz de 20 filas y 5 columnas con valores flotantes equiespaciados en el intervalo
cerrado [1, 10].
• Un array unidimensional con 128 valores aleatorios de una distribución normal 𝜇 =
1, 𝜎 = 2.
• Un array unidimensional con 15 valores aleatorios de una muestra 1, X, 2 donde la
probabilidad de que gane el equipo local es del 50%, la probabilidad de que empaten
es del 30% y la probabilidad de que gane el visitante es del 20%.
Constantes
>>> np.Inf
inf
>>> np.nan
nan
(continué en la próxima página)
>>> np.e
2.718281828459045
>>> np.pi
3.141592653589793
Los arrays multidimensionales de NumPy están indexados por unos ejes que establecen la
forma en la que debemos acceder a sus elementos. Véase el siguiente diagrama:
Arrays unidimensionales
>>> values
array([10, 11, 12, 13, 14, 15])
>>> values[2]
12
>>> values[-3]
13
3
Imagen de Harriet Dashnow, Stéfan van der Walt y Juan Núñez-Iglesias en O’Reilly.
>>> values
array([10, 11, 12, 13, 14, 15])
>>> values
array([26, 11, 12, 13, 14, 15])
>>> values
array([10, 11, 12, 13, 14, 15])
Nota: La función np.delete() no es destructiva. Devuelve una copia modificada del array.
>>> values
array([10, 11, 12, 13, 14, 15])
Para ambas funciones también es posible añadir varios elementos de una sola vez:
>>> values
array([10, 11, 12, 13, 14, 15])
Arrays multidimensionales
Partimos del siguiente array bidimensional (matriz) para ejemplificar las distintas
operaciones:
>>> values
array([[ 1, 2, 3, 4],
[ 5, 6, 7, 8],
[ 9, 10, 11, 12]])
>>> values[0, 0]
1
>>> values[-1, -1]
12
>>> values[1, 2]
7
Importante: Todos estos accesos crean una copia (vista) del array original. Esto significa
que, si modificamos un valor en el array copia, se ve reflejado en el original. Para evitar esta
situación podemos usar la función np.copy() y desvincular la vista de su fuente.
>>> values
array([[ 1, 2, 3, 4],
[ 5, 6, 7, 8],
[ 9, 10, 11, 12]])
>>> values
array([[100, 2, 30, 4],
[ 55, 66, 70, 88],
[ 9, 10, 110, 12]])
>>> values
array([[ 1, 2, 3, 4],
[ 5, 6, 7, 8],
[ 9, 10, 11, 12]])
Truco: Tener en cuenta que axis=0 hace referencia a filas y axis=1 hace referencia a
columnas tal y como describe el diagrama del comienzo de la sección.
>>> values
array([[1, 2],
[3, 4]])
>>> values
array([[1, 2],
[3, 4]])
Ejercicio
Utilizando las operaciones de modificación, borrado e inserción, convierta la siguiente matriz:
⎡ ⎤
17 12 31
⎢49 11 51⎥
⎢ ⎥
⎣21 31 62⎦
63 75 22
en esta:
⎡ ⎤
17 12 31 63
⎣49 11 51 75⎦
21 31 62 22
y luego en esta:
⎡ ⎤
17 12 31 63
⎣49 49 49 63⎦
21 31 62 63
Apilando matrices
Hay veces que nos interesa combinar dos matrices (arrays en general). Una de los mecanismos
que nos proporciona NumPy es el apilado.
Apilado vertical:
>>> m1
array([[68, 68],
[10, 50],
[87, 92]])
>>> m2
array([[63, 80]])
Apilado horizontal:
>>> m1
array([[51, 50],
[52, 15],
[14, 21]])
>>> m2
array([[18],
[52],
[ 1]])
Repitiendo elementos
Repetición por ejes: El parámetro de repetición indica el número de veces que repetimos
el array completo por cada eje:
>>> values
array([[1, 2],
[3, 4],
[5, 6]])
>>> np.repeat(values, 2)
array([1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6])
Es bastante común acceder a elementos de una matriz (array en general) tomando como
referencia su diagonal. Para ello, NumPy nos provee de ciertos mecanismos que veremos a
continuación.
Para ejemplificarlo, partiremos del siguiente array:
>>> values
array([[73, 86, 90, 20],
[96, 55, 15, 48],
[38, 63, 96, 95],
[13, 87, 32, 96]])
>>> np.diag(values) # k = 0
array([73, 55, 96, 96])
NumPy también provee un método np.diag_indices() que retorna los índices de los
elementos de la diagonal principal, con lo que podemos modificar sus valores directamente:
>>> values
array([[73, 86, 90, 20],
[96, 55, 15, 48],
[38, 63, 96, 95],
[13, 87, 32, 96]])
>>> di = np.diag_indices(values.shape[0])
>>> di
(array([0, 1, 2, 3]), array([0, 1, 2, 3]))
>>> values[di] = 0
>>> values
array([[ 0, 86, 90, 20],
[96, 0, 15, 48],
[38, 63, 0, 95],
[13, 87, 32, 0]])
Operaciones lógicas
Indexado booleano
El indexado booleano es una operación que permite conocer (a nivel de elemento) si un array
cumple o no con una determinada condición:
>>> values
array([[60, 47, 34, 38],
[43, 63, 37, 68],
[58, 28, 31, 43],
[32, 65, 32, 96]])
>>> values
array([[-1, 47, 34, 38],
[43, -1, 37, -1],
[-1, 28, 31, 43],
[32, -1, 32, -1]])
Las condiciones pueden ser más complejas e incorporar operadores lógicos | (or) y & (and):
>>> values
array([[60, 47, 34, 38],
[43, 63, 37, 68],
[58, 28, 31, 43],
[32, 65, 32, 96]])
Ejercicio
Extraiga todos los números impares de la siguiente matriz:
⎡ ⎤
10 11 12 13
values = ⎣14 15 16 17⎦
18 19 20 21
Si lo que nos interesa es obtener los índices del array que satisfacen una determinada
condición, NumPy nos proporciona el método where() cuyo comportamiento se ejemplifica
a continuación:
>>> values
array([[60, 47, 34, 38],
[43, 63, 37, 68],
[58, 28, 31, 43],
[32, 65, 32, 96]])
>>> idx
(array([0, 1, 1, 2, 3, 3]), array([0, 1, 3, 0, 1, 3]))
>>> values[idx]
array([60, 63, 68, 58, 65, 96])
Ejercicio
Partiendo de una matriz de 10 filas y 10 columnas con valores aleatorios enteros en el intervalo
[0, 100], realice las operaciones necesarias para obtener una matriz de las mismas dimensiones
donde:
• Todos los elementos de la diagonal sean 50.
Comparando arrays
Dados dos arrays podemos compararlos usando el operador == del mismo modo que con
cualquier otro objeto en Python. La cuestión es que el resultado se evalúa a nivel de elemento:
>>> m1 == m2
array([[ True, True],
[ True, True]])
Operaciones de conjunto
Al igual que existen operaciones sobre conjuntos en Python, también podemos llevarlas a
cabo sobre arrays en NumPy.
Unión de arrays
𝑥∪𝑦
>>> x
array([ 9, 4, 11, 3, 14, 5, 13, 12, 7, 14])
>>> y
array([17, 9, 19, 4, 18, 4, 7, 13, 11, 10])
>>> np.union1d(x, y)
array([ 3, 4, 5, 7, 9, 10, 11, 12, 13, 14, 17, 18, 19])
Intersección de arrays
𝑥∩𝑦
>>> x
array([ 9, 4, 11, 3, 14, 5, 13, 12, 7, 14])
>>> y
array([17, 9, 19, 4, 18, 4, 7, 13, 11, 10])
>>> np.intersect1d(x, y)
array([ 4, 7, 9, 11, 13])
Diferencia de arrays
𝑥∖𝑦
>>> x
array([ 9, 4, 11, 3, 14, 5, 13, 12, 7, 14])
>>> y
array([17, 9, 19, 4, 18, 4, 7, 13, 11, 10])
>>> np.setdiff1d(x, y)
array([ 3, 5, 12, 14])
Ordenación de arrays
En términos generales, existen dos formas de ordenar cualquier estructura de datos, una
que modifica «in-situ» los valores (destructiva) y otra que devuelve «nuevos» valores (no
destructiva). En el caso de NumPy también es así.
>>> values
array([23, 24, 92, 88, 75, 68, 12, 91, 94, 24, 9, 21, 42, 3, 66])
>>> values
array([ 3, 9, 12, 21, 23, 24, 24, 42, 66, 68, 75, 88, 91, 92, 94])
>>> values
array([[52, 23, 90, 46],
[61, 63, 74, 59],
[75, 5, 58, 70],
[21, 7, 80, 52]])
Contando valores
Otra de las herramientas útiles que proporciona NumPy es la posibilidad de contar el número
de valores que existen en un array en base a ciertos criterios.
Para ejemplificarlo, partiremos de un array unidimensional con valores de una distribución
aleatoria uniforme en el intervalo [1, 10]:
>>> randomized
array([ 7, 9, 7, 8, 3, 7, 6, 4, 3, 9, 3, 1, 6, 7, 10, 4, 8,
1, 3, 3, 8, 5, 4, 7, 5, 8, 8, 3, 10, 1, 7, 10, 3, 10,
2, 9, 5, 1, 2, 4, 4, 10, 5, 10, 5, 2, 5, 2, 10, 3, 4,
...
Valores únicos:
>>> np.unique(randomized)
array([ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10])
>>> np.count_nonzero(randomized)
1000
Operaciones aritméticas
Una de las grandes ventajas del uso de arrays numéricos en NumPy es la posibilidad de
trabajar con ellos como si fueran objetos «simples» pero sacando partido de la aritmética
vectorial. Esto redunda en una mayor eficiencia y rapidez de cómputo.
Cuando operamos entre arrays de las mismas dimensiones, las operaciones aritméticas se
realizan elemento a elemento (ocupando misma posición) y el resultado, obviamente, tiene
las mismas dimensiones:
>>> m1
array([[21, 86, 45],
[31, 36, 78],
[31, 64, 70]])
>>> m2
array([[58, 67, 17],
[99, 53, 9],
[92, 42, 75]])
>>> m1 + m2
array([[ 79, 153, 62],
[130, 89, 87],
[123, 106, 145]])
>>> m1 - m2
(continué en la próxima página)
>>> m1 * m2
array([[1218, 5762, 765],
[3069, 1908, 702],
[2852, 2688, 5250]])
Cuando operamos entre arrays con dimensiones diferentes, siempre y cuando se cumplan
ciertas restricciones en tamaños de filas y/o columnas, lo que se produce es un «broadcasting»
(o difusión) de los valores.
Suma con array «fila»:
>>> m
array([[9, 8, 1],
[7, 6, 7]])
>>> v
array([[2, 3, 6]])
>>> m + v # broadcasting
array([[11, 11, 7],
[ 9, 9, 13]])
>>> m
array([[9, 8, 1],
[7, 6, 7]])
>>> m + v # broadcasting
array([[10, 9, 2],
[13, 12, 13]])
Al igual que ocurría en los casos anteriores, si operamos con un array y un escalar, éste
último será difundido para abarcar el tamaño del array:
>>> m
array([[9, 8, 1],
[7, 6, 7]])
>>> m + 5
array([[14, 13, 6],
[12, 11, 12]])
>>> m - 5
array([[ 4, 3, -4],
[ 2, 1, 2]])
>>> m * 5
array([[45, 40, 5],
[35, 30, 35]])
>>> m / 5
array([[1.8, 1.6, 0.2],
[1.4, 1.2, 1.4]])
>>> m // 5
array([[1, 1, 0],
[1, 1, 1]])
>>> m ** 5
(continué en la próxima página)
Operaciones unarias
Funciones universales
Las funciones universales «ufunc» son funciones que operan sobre arrays elemento a
elemento. Existen muchas funciones universales definidas en Numpy, parte de ellas operan
sobre dos arrays y parte sobre un único array.
Un ejemplo de algunas de estas funciones:
>>> values
array([[48.32172375, 24.89651106, 77.49724241],
[77.81874191, 22.54051494, 65.11282444],
[ 5.54960482, 59.06720303, 62.52817198]])
>>> np.sqrt(values)
array([[6.95138287, 4.98964037, 8.80325181],
[8.82149318, 4.74768522, 8.06925179],
[2.35575992, 7.68551905, 7.9074757 ]])
>>> np.sin(values)
array([[-0.93125201, -0.23403917, 0.86370435],
[ 0.66019205, -0.52214693, 0.75824777],
[-0.66953344, 0.58352079, -0.29903488]])
>>> np.ceil(values)
array([[49., 25., 78.],
[78., 23., 66.],
[ 6., 60., 63.]])
>>> np.floor(values)
array([[48., 24., 77.],
[77., 22., 65.],
[ 5., 59., 62.]])
>>> np.log(values)
array([[3.87788123, 3.21472768, 4.35024235],
(continué en la próxima página)
Reduciendo el resultado
NumPy nos permite aplicar cualquier función sobre un array reduciendo el resultado por
alguno de sus ejes. Esto abre una amplia gama de posibilidades.
A modo de ilustración, veamos un par de ejemplos con la suma y el producto:
>>> values
array([[8, 2, 7],
[2, 0, 6],
[6, 3, 4]])
Ejercicio
Compruebe que, para 𝜃 = 2𝜋 (radianes) y 𝑘 = 20 se cumple la siguiente igualdad del producto
infinito de Euler:
sin(𝜃)
(︂ )︂ (︂ )︂ (︂ )︂ 𝑘 (︂ )︂
𝜃 𝜃 𝜃 𝜃
cos · cos · cos cos 𝑖 ≈
∏︁
··· =
2 4 8 𝑖=1
2 𝜃
Funciones estadísticas
NumPy proporciona una gran cantidad de funciones estadísticas que pueden ser aplicadas
sobre arrays.
Veamos algunas de ellas:
>>> dist
array([[-6.79006504, -0.01579498, -0.29182173, 0.3298951 , -5.30598975],
[ 3.10720923, -4.09625791, -7.60624152, 2.3454259 , 9.23399023],
[-7.4394269 , -9.68427195, 3.04248586, -5.9843767 , 1.536578 ],
[ 3.33953286, -8.41584411, -9.530274 , -2.42827813, -7.34843663],
[ 7.1508544 , 5.51727548, -3.20216834, -5.00154367, -7.15715252]])
>>> np.mean(dist)
-2.1877878715377777
>>> np.std(dist)
5.393254994089515
>>> np.median(dist)
-3.2021683412383295
Máximos y mínimos
>>> values
array([[66, 54, 33, 15, 58],
[55, 46, 39, 16, 38],
[73, 75, 79, 25, 83],
[81, 30, 22, 32, 8],
[92, 25, 82, 10, 90]])
>>> np.min(values)
8
>>> np.max(values)
(continué en la próxima página)
Si lo que interesa es obtener los índices de aquellos elementos con valores máximos o
mínimos, podemos hacer uso de las funciones argmax() y argmin() respectivamente.
Veamos un ejemplo donde obtenemos los valores máximos por columnas (mediante sus
índices):
>>> values
array([[66, 54, 33, 15, 58],
[55, 46, 39, 16, 38],
[73, 75, 79, 25, 83],
[81, 30, 22, 32, 8],
[92, 25, 82, 10, 90]])
>>> idx
array([4, 2, 4, 3, 4])
Vectorizando funciones
Una de las ventajas de trabajar con arrays numéricos en NumPy es sacar provecho de
la optimización que se produce a nivel de la propia estructura de datos. En el caso de
que queramos implementar una función propia para realizar una determinada acción, sería
deseable seguir aprovechando esa característica.
Veamos un ejemplo en el que queremos realizar el siguiente cálculo entre dos matrices 𝐴 y
𝐵:
Las dos matrices de partida tienen 9M de valores aleatorios entre -100 y 100:
Una primera aproximación para aplicar esta función a cada elemento de las matrices de
entrada sería la siguiente:
>>> %%timeit
... for i in range(A.shape[0]):
... for j in range(A.shape[1]):
... result[i, j] = customf(A[i, j], B[i, j])
...
3 s ± 23.8 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
Con un pequeño detalle podemos mejorar el rendimiento de la función que hemos diseñado
anteriormente. Se trata de decorarla con np.vectorize con lo que estamos otorgándole un
comportamiento distinto y enfocado al procesamiento de arrays numéricos:
>>> @np.vectorize
... def customf(a, b):
... if a > b:
... return a + b
... elif a < b:
... return a - b
... else:
... return 0
...
Dado que ahora ya se trata de una función vectorizada podemos aplicarla directamente
a las matrices de entrada (aprovechamos para medir su tiempo de ejecución):
Hemos obtenido una mejora de 2.32x con respecto al uso de funciones simples.
Truco: La mejora de rendimiento se aprecia más claramente a medida que los tamaños de
las matrices (arrays) de entrada son mayores.
Consejo: El uso de funciones lambda puede ser muy útil en vectorización: np.
vectorize(lambda a, b: return a + b).
Ejercicio
1. Cree dos matrices cuadradas de 20x20 con valores aleatorios flotantes uniformes en el
intervalo [0, 1000)
2. Vectorice una función que devuelva la media (elemento a elemento) entre las dos
matrices.
3. Realice la misma operación que en 2) pero usando suma de matrices y división por
escalar.
4. Compute los tiempos de ejecución de 2) y 3)
NumPy tiene una sección dedicada al álgebra lineal cuyas funciones pueden resultar muy
interesantes según el contexto en el que estemos trabajando.
Producto de matrices
Si bien hemos hablado del producto de arrays elemento a elemento, NumPy nos permite
hacer la multiplicación clásica de matrices:
>>> m1
array([[1, 8, 4],
[8, 7, 1],
[1, 3, 8]])
>>> m2
(continué en la próxima página)
>>> m1 @ m2
array([[77, 53, 31],
[72, 72, 72],
[36, 49, 29]])
Ejercicio
[︂ ]︂
1 2
Compruebe que la matriz satisface la ecuación matricial: 𝑋 2 − 6𝑋 − 𝐼 = 0 donde 𝐼
3 5
es la matriz identidad de orden 2.
El cálculo del determinante es una operación muy utilizada en álgebra lineal. Lo podemos
realizar en NumPy de la siguiente manera:
>>> m
array([[4, 1, 6],
[4, 8, 8],
[2, 1, 7]])
>>> np.linalg.det(m)
108.00000000000003
>>> m
array([[4, 1, 6],
[4, 8, 8],
[2, 1, 7]])
>>> m_inv
array([[ 0.44444444, -0.00925926, -0.37037037],
[-0.11111111, 0.14814815, -0.07407407],
[-0.11111111, -0.01851852, 0.25925926]])
>>> m
array([[1, 2, 3],
[4, 5, 6]])
>>> m.T
array([[1, 4],
[2, 5],
[3, 6]])
Ejercicio
Dadas las matrices:
[︂ ]︂ [︂ ]︂
1 −2 1 4 0 −1
𝐴= ; 𝐵=
3 0 1 −2 1 0
>>> m
array([[4, 1, 6],
[4, 8, 8],
[2, 1, 7]])
Ejercicio
⎡ ⎤
4 5 −1
Dada la matriz 𝐴 = ⎣−3 −4 1 ⎦ calcule: 𝐴2 , 𝐴3 , . . . , 𝐴128
−3 −4 0
¿Nota algo especial en los resultados?
NumPy también nos permite resolver sistemas de ecuaciones lineales. Para ello debemos
modelar nuestro sistema a través de arrays.
Veamos un ejemplo en el que queremos resolver el siguiente sistema de ecuaciones lineales:
⎧
⎨𝑥1 + 2𝑥3 = 1
⎛ ⎞⎛ ⎞ ⎛ ⎞
⎪ 1 0 2 𝑥1 1
𝑥1 − 𝑥2 = −2 =⇒ ⎝ 1 −1 0 ⎠ ⎝ 𝑥 2
⎠ = ⎝ −2 ⎠ =⇒ 𝒜𝒳 = ℬ
0 1 1 𝑥3 −1
⎪
𝑥2 + 𝑥3 = −1
⎩
>>> A
array([[ 1, 0, 2],
[ 1, -1, 0],
[ 0, 1, 1]])
>>> B
array([[ 1],
[-2],
[-1]])
>>> np.linalg.solve(A, B)
array([[-7.],
[-5.],
[ 4.]])
>>> np.dot(np.linalg.inv(A), B)
array([[-7.],
[-5.],
[ 4.]])
Ejercicio
Resuelva el siguiente sistema de ecuaciones lineales:
⎧
⎨3𝑥 + 4𝑦 − 𝑧 = 8
⎪
5𝑥 − 2𝑦 + 𝑧 = 4
⎪
2𝑥 − 2𝑦 + 𝑧 = 1
⎩
8.3 pandas
pandas es un paquete open-source que nos proporciona una forma sencilla y potente de
trabajar con estructuras de datos a través de múltiples herramientas para su análisis.1
8.3.1 Series
Podríamos pensar en una serie como un ndarray en el que cada valor tiene asignado una
etiqueta (índice) y además admite un título (nombre).
Nota: El índice por defecto se crea con números enteros positivos empezando desde
0.
>>> items = { a : 1, b : 2, c : 3}
>>> pd.Series(items)
a 1
b 2
c 3
dtype: int64
Todas las series que hemos visto hasta ahora no tienen asignado ningún nombre. Lo podemos
hacer usando el parámetro name en la creación de la serie:
Ejercicio
Cree una serie de pandas con valores enteros en el intervalo [1, 26] y etiquetas
ABCDEFGHIJKLMNOPQRSTUVWXYZ . Busque una manera programática (no manual) de hacerlo
(recuerde el módulo string).
>>> data
{ Apple : 147000,
Samsung : 267937,
Google : 135301,
Microsoft : 163000,
Huawei : 197000,
Dell : 158000,
Facebook : 58604,
Foxconn : 878429,
Sony : 109700}
Índice de la serie:
>>> employees.index
Index([ Apple , Samsung , Google , Microsoft , Huawei , Dell , Facebook ,
Foxconn , Sony ],
dtype= object )
Valores de la serie:
>>> employees.values
array([147000, 267937, 135301, 163000, 197000, 158000, 58604, 878429,
109700])
2
Fuente: Wikipedia.
Tipo de la serie:
>>> employees.dtype
dtype( int64 )
Nombre de la serie:
>>> employees.name
Tech Employees
>>> employees.nbytes
72
>>> employees.size
9
Selección de registros
>>> employees
Apple 147000
Samsung 267937
Google 135301
Microsoft 163000
Huawei 197000
Dell 158000
Facebook 58604
Foxconn 878429
Sony 109700
Name: Tech Employees, dtype: int64
Para acceder a los registros por su posición (índice numérico) basta usar corchetes como ya
se ha visto en cualquier secuencia:
>>> employees[0]
147000
>>> employees[-1]
109700
>>> employees[2:5]
Google 135301
Microsoft 163000
Huawei 197000
Name: Tech Employees, dtype: int64
>>> employees[1:6:2]
Samsung 267937
Microsoft 163000
Dell 158000
Name: Tech Employees, dtype: int64
El atributo iloc es un alias (algo más expresivo) que permite realizar las mismas operaciones
de indexado (con corchetes) que hemos visto anteriormente:
>>> employees.iloc[1:6:2]
Samsung 267937
Microsoft 163000
Dell 158000
Name: Tech Employees, dtype: int64
Truco: Python, y en este caso pandas, se dicen «0-index» porque sus índices (posiciones)
comienzan en cero.
En el caso de aquellas series que dispongan de un índice con etiquetas, podemos acceder a
sus registros utilizando las mismas:
>>> employees[ Apple ] # equivalente a employees.Apple
147000
El atributo loc es un alias (algo más expresivo) que permite realizar las mismas operaciones
de indexado (con corchetes) que hemos visto anteriormente:
A nivel exploratorio, es bastante cómodo acceder a una porción inicial (o final) de los datos
que manejamos. Esto se puede hacer de forma muy sencilla con series:
>>> employees.head(3)
Apple 147000
Samsung 267937
Google 135301
Name: Tech Employees, dtype: int64
>>> employees.tail(3)
Facebook 58604
Foxconn 878429
Sony 109700
Name: Tech Employees, dtype: int64
Si tenemos en cuenta que una serie contiene valores en formato ndarray podemos concluir
que las operaciones sobre arrays son aplicables al caso de las series. Veamos algunos ejemplos
de operaciones que podemos aplicar sobre series.
Operaciones lógicas
Supongamos que queremos filtrar aquellas empresas que tengan más de 200000
trabajadores/as:
Hemos obtenido una serie «booleana». Si queremos aplicar esta «máscara», podemos hacerlo
con indexado:
Ordenación
>>> employees.sort_values()
Facebook 58604
Sony 109700
Google 135301
Apple 147000
Dell 158000
Microsoft 163000
Huawei 197000
(continué en la próxima página)
>>> employees.sort_index()
Apple 147000
Dell 158000
Facebook 58604
Foxconn 878429
Google 135301
Huawei 197000
Microsoft 163000
Samsung 267937
Sony 109700
Name: Tech Employees, dtype: int64
Contando valores
Si queremos obtener una «tabla de frecuencias» podemos contar los valores que existen en
nuestra serie:
>>> marks.value_counts()
5 3
3 2
6 2
8 2
2 1
7 1
dtype: int64
Vinculado con el caso anterior, podemos obtener el número de valores únicos en la serie:
>>> marks.nunique()
6
El método count() devuelve el número de valores «no nulos» que contiene la serie:
Operaciones aritméticas
Para el caso de operaciones entre series, vamos a ejemplificarlo con las dos siguientes3 :
>>> employees
Apple 147000
Samsung 267937
Google 135301
Microsoft 163000
Huawei 197000
Dell 158000
Facebook 58604
Foxconn 878429
Sony 109700
Name: Tech Employees, dtype: int64
(continué en la próxima página)
3
Los datos de ingresos («revenues») están en billones (americanos) de dólares.
>>> revenues
Apple 274515
Samsung 200734
Google 182527
Microsoft 143015
Huawei 129184
Dell 92224
Facebook 85965
Foxconn 181945
Sony 84893
Name: Tech Revenues, dtype: int64
Truco: Tener en cuenta que las operaciones se realizan entre registros que tienen el mismo
índice (etiqueta).
Funciones estadísticas
Existen multitud de funciones estadísticas que podemos aplicar a una serie. Dependiendo
del tipo de dato con el que estamos trabajando, serán más útiles unas que otras. Veamos dos
funciones a modo de ejemplo:
>>> employees.mean()
234996.77777777778
>>> employees.std()
248027.7840619765
Máximos y mínimos
>>> employees.min()
58604
>>> employees.max()
878429
>>> employees.idxmin()
Facebook
>>> employees.idxmax()
Foxconn
>>> employees.nsmallest(3)
Facebook 58604
Sony 109700
Google 135301
Name: Tech Employees, dtype: int64
>>> employees.nlargest(3)
Foxconn 878429
Samsung 267937
Huawei 197000
Name: Tech Employees, dtype: int64
Exportación de series
Suele ser bastante habitual intercambiar datos en distintos formatos (y aplicaciones). Para
ello, pandas nos permite exportar una serie a multitud de formatos. Veamos algunos de ellos:
Exportación de serie a lista:
>>> employees.to_list()
[147000, 267937, 135301, 163000, 197000, 158000, 58604, 878429, 109700]
>>> employees.to_dict()
{ Apple : 147000,
Samsung : 267937,
Google : 135301,
Microsoft : 163000,
Huawei : 197000,
Dell : 158000,
Facebook : 58604,
Foxconn : 878429,
Sony : 109700}
>>> employees.to_csv()
,Tech Employees\nApple,147000\nSamsung,267937\nGoogle,135301\nMicrosoft,163000\
˓→nHuawei,197000\nDell,158000\nFacebook,58604\nFoxconn,878429\nSony,109700\n
>>> employees.to_json()
{"Apple":147000,"Samsung":267937,"Google":135301,"Microsoft":163000,"Huawei
˓→":197000,"Dell":158000,"Facebook":58604,"Foxconn":878429,"Sony":109700}
>>> employees.to_frame()
Tech Employees
Apple 147000
Samsung 267937
Google 135301
Microsoft 163000
Huawei 197000
Dell 158000
Facebook 58604
Foxconn 878429
Sony 109700
8.3.2 DataFrames
Un DataFrame es una estructura tabular compuesta por series. Se trata del tipo de datos
fundamental en pandas y sobre el que giran la mayoría de operaciones que podemos realizar.
Creación de un DataFrame
Cada elemento del diccionario se convierte en una columna, donde su clave es el nombre y
sus valores se despliegan en «vertical»:
>>> pd.DataFrame(data)
A B
0 1 4
1 2 5
2 3 6
Cada elemento de la lista se convierte en una fila. Las claves de cada diccionario serán los
nombres de las columnas y sus valores se despliegan en «horizontal»:
>>> pd.DataFrame(data)
A B C
0 1 2 3
1 4 5 6
Cada elemento de la lista se convierte en una fila y sus valores se despliegan en «horizontal».
Los nombres de las columnas deben pasarse como parámetro opcional:
>>> employees
Apple 147000
Samsung 267937
Google 135301
Microsoft 163000
Huawei 197000
Dell 158000
Facebook 58604
Foxconn 878429
Sony 109700
Name: Tech Employees, dtype: int64
>>> revenues
Apple 274515
Samsung 200734
(continué en la próxima página)
Ejercicio
Cree el siguiente DataFrame en Pandas5 :
5
Datos extraídos de Wikipedia.
La superficie (Area) está expresada en km2 y las provincias corresponden con LPGC: Las
Palmas de Gran Canaria y SCTF: Santa Cruz de Tenerife.
Nota: En el caso anterior se puede observar que el índice toma un nombre A. Esto se puede
conseguir directamente asignando un valor a df.index.name.
Podemos añadir un parámetro (en la creación) para especificar los valores que queremos
incluir en el índice:
Ejercicio
Convierta la columna Island en el índice de democan. El DataFrame debería de quedar así:
>>> df
Population Area Province
Island
Gran Canaria 855521 1560.10 LPGC
Tenerife 928604 2034.38 SCTF
La Palma 83458 708.32 SCTF
Lanzarote 155812 845.94 LPGC
La Gomera 21678 369.76 SCTF
El Hierro 11147 278.71 SCTF
Fuerteventura 119732 1659.00 LPGC
Nota: Todas estas funciones tienen su equivalente para escribir datos en los distintos
formatos. En vez de read_ habría que usar el prefijo to_. Por ejemplo: .to_csv(), .to_json()
o .to_sql()
A modo de ilustración, vamos a leer el contenido del fichero tech.csv que contiene la lista
de las mayores empresas tecnológicas por ingresos totales (en millones de dólares)2 .
Usaremos la función read_csv() que espera la coma como separador de campos. Este fichero
está delimitado por tabuladores, por lo que especificaremos esta circunstancia mediante el
parámetro delimiter. Igualmente, vamos a indicar que se use la primera columna Company
como índice del DataFrame con el parámetro index_col:
>>> df
Revenue Employees City Country
Company
Apple 274515 147000 California United States
Samsung Electronics 200734 267937 Suwon South Korea
Alphabet 182527 135301 California United States
Foxconn 181945 878429 New Taipei City Taiwan
Microsoft 143015 163000 Washington United States
Huawei 129184 197000 Shenzhen China
Dell Technologies 92224 158000 Texas United States
Facebook 85965 58604 California United States
Sony 84893 109700 Tokyo Japan
Hitachi 82345 350864 Tokyo Japan
Intel 77867 110600 California United States
IBM 73620 364800 New York United States
Tencent 69864 85858 Shenzhen China
Panasonic 63191 243540 Osaka Japan
Lenovo 60742 71500 Hong Kong China
HP Inc. 56639 53000 California United States
LG Electronics 53625 75000 Seoul South Korea
Truco: Se suele usar df como nombre para las variables tipo DataFrame.
Ejercicio
Cargue el conjunto de datos democan desde democan.csv en un DataFrame df indicando que
la columna Island es el índice.
También es posible cargar el «dataset» a través de la URL que conseguimos con botón derecho:
copiar enlace.
Características de un DataFrame
Para «echar un vistazo» a los datos, existen dos funciones muy recurridas:
>>> df.head()
Revenue Employees City Country
(continué en la próxima página)
>>> df.tail()
Revenue Employees City Country
Company
Tencent 69864 85858 Shenzhen China
Panasonic 63191 243540 Osaka Japan
Lenovo 60742 71500 Hong Kong China
HP Inc. 56639 53000 California United States
LG Electronics 53625 75000 Seoul South Korea
Pandas ofrece algunas funciones que proporcionan un cierto «resumen» de los datos a nivel
descriptivo. Veamos algunas de ellas.
Información sobre columnas:
>>> df.info()
<class pandas.core.frame.DataFrame >
Index: 17 entries, Apple to LG Electronics
Data columns (total 4 columns):
# Column Non-Null Count Dtype
--- ------ -------------- -----
0 Revenue 17 non-null int64
1 Employees 17 non-null int64
2 City 17 non-null object
3 Country 17 non-null object
dtypes: int64(2), object(2)
memory usage: 680.0+ bytes
>>> df.describe()
Revenue Employees
(continué en la próxima página)
Uso de memoria:
>>> df.memory_usage()
Index 692
Revenue 136
Employees 136
City 136
Country 136
dtype: int64
Atributos de un DataFrame
Tamaños y dimensiones:
>>> df.index
Index([ Apple , Samsung Electronics , Alphabet , Foxconn , Microsoft ,
Huawei , Dell Technologies , Facebook , Sony , Hitachi , Intel ,
IBM , Tencent , Panasonic , Lenovo , HP Inc. , LG Electronics ],
(continué en la próxima página)
>>> df.columns
Index([ Revenue , Employees , City , Country ], dtype= object )
>>> df.values
array([[274515, 147000, California , United States ],
[200734, 267937, Suwon , South Korea ],
[182527, 135301, California , United States ],
[181945, 878429, New Taipei City , Taiwan ],
[143015, 163000, Washington , United States ],
[129184, 197000, Shenzhen , China ],
[92224, 158000, Texas , United States ],
[85965, 58604, California , United States ],
[84893, 109700, Tokyo , Japan ],
[82345, 350864, Tokyo , Japan ],
[77867, 110600, California , United States ],
[73620, 364800, New York , United States ],
[69864, 85858, Shenzhen , China ],
[63191, 243540, Osaka , Japan ],
[60742, 71500, Hong Kong , China ],
[56639, 53000, California , United States ],
[53625, 75000, Seoul , South Korea ]], dtype=object)
Acceso a un DataFrame
>>> df
Revenue Employees City Country
Company
Apple 274515 147000 California United States
Samsung Electronics 200734 267937 Suwon South Korea
Alphabet 182527 135301 California United States
Foxconn 181945 878429 New Taipei City Taiwan
Microsoft 143015 163000 Washington United States
Huawei 129184 197000 Shenzhen China
Dell Technologies 92224 158000 Texas United States
Facebook 85965 58604 California United States
Sony 84893 109700 Tokyo Japan
Hitachi 82345 350864 Tokyo Japan
(continué en la próxima página)
Acceso a filas
>>> df.iloc[0]
Revenue 274515
Employees 147000
City California
Country United States
Name: Apple, dtype: object
>>> df.iloc[-1]
Revenue 53625
Employees 75000
City Seoul
Country South Korea
Name: LG Electronics, dtype: object
>>> df.iloc[3:5]
Revenue Employees City Country
Company
Foxconn 181945 878429 New Taipei City Taiwan
Microsoft 143015 163000 Washington United States
Si queremos acceder a las filas de un conjunto de datos mediante la etiqueta del registro
usamos el atributo loc:
Acceso a columnas
>>> df_reordered.head()
City Country Revenue Employees
Company
Apple California United States 274515 147000
Samsung Electronics Suwon South Korea 200734 267937
Alphabet California United States 182527 135301
Foxconn New Taipei City Taiwan 181945 878429
Microsoft Washington United States 143015 163000
Si mezclamos los dos accesos anteriores podemos seleccionar datos de forma muy precisa.
Como siempre, partimos del «dataset» de empresas tecnológicas:
>>> df.head()
Revenue Employees City Country
Company
Apple 274515 147000 California United States
Samsung Electronics 200734 267937 Suwon South Korea
Alphabet 182527 135301 California United States
Foxconn 181945 878429 New Taipei City Taiwan
Microsoft 143015 163000 Washington United States
>>> df.iloc[0, 0]
274515
Acceso a las tres últimas filas (empresas) y a las dos primeras columnas:
Acceso a las filas que van desde «Apple» a «Huawei» y a las columnas que van
desde «Revenue» hasta «City»:
Selección condicional
Es posible aplicar ciertas condiciones en la selección de los datos para obtener el subconjunto
que estemos buscando. Veremos distintas aproximaciones a esta técnica.
Supongamos que queremos seleccionar aquellas empresas con base en Estados Unidos.
Si aplicamos la condición sobre la columna obtendremos una serie de tipo «booleano» en la
que se indica para qué registros se cumple la condición (incluyendo el índice):
Si aplicamos esta «máscara» al conjunto original de datos, obtendremos las empresas que
estamos buscando:
Los operadores lógicos que se pueden utilizar para combinar condiciones de selección son los
siguientes:
Operador Significado
| «or» lógico
& «and» lógico
~ «not» lógico
^ «xor» lógico
>>> df[mask]
Revenue Employees City Country
Company
Apple 274515 147000 California United States
Alphabet 182527 135301 California United States
Facebook 85965 58604 California United States
Sony 84893 109700 Tokyo Japan
Hitachi 82345 350864 Tokyo Japan
Intel 77867 110600 California United States
HP Inc. 56639 53000 California United States
Ejercicio
Obtenga los siguientes subconjuntos del «dataset» democan:
# Use .loc
Population Area Province
Island
El Hierro 11147 278.71 SCTF
La Gomera 21678 369.76 SCTF
# Use .loc
Island
Gran Canaria LPGC
Tenerife SCTF
La Palma SCTF
Lanzarote LPGC
La Gomera SCTF
El Hierro SCTF
Fuerteventura LPGC
Name: Province, dtype: object
# Use .iloc
Island
Gran Canaria 1560.10
La Palma 708.32
La Gomera 369.76
Fuerteventura 1659.00
Name: Area, dtype: float64
Pandas provee una alternativa para la selección condicional de registros a través de la función
query(). Admite una sintaxis de consulta mediante operadores de comparación.
Veamos las mismas consultas de ejemplo que para el apartado anterior:
Truco: Si los nombres de columna contienen espacios, se puede hacer referencias a ellas con
comillas invertidas. Por ejemplo: Total Stock .
Comparativa en consultas
Hemos visto dos métodos para realizar consultas (o filtrado) en un DataFrame: usando
selección booleana con corchetes y usando la función query. ¿Ambos métodos son igual de
eficientes en términos de rendimiento?
Haremos una comparativa muy simple para tener, al menos, una idea de sus órdenes de
magnitud. En primer lugar creamos un DataFrame con 3 columnas y 1 millón de valores
aleatorios enteros en cada una de ellas:
>>> data = {
... A : np.random.randint(1, 100, size=size),
... B : np.random.randint(1, 100, size=size),
... C : np.random.randint(1, 100, size=size)
(continué en la próxima página)
>>> df = pd.DataFrame(data)
>>> df.shape
(1000000, 3)
Ahora realizaremos la misma consulta sobre el DataFrame aplicando los métodos ya vistos:
Sin que esto sea en modo alguna concluyente, da la sensación de que query() añade un cierto
«overhead»7 al filtrado y aumentan los tiempos de cómputo.
Modificación de un DataFrame
Partiendo del acceso a los datos que ya hemos visto, podemos asignar valores sin mayor
dificultad.
Pero antes de modificar el DataFrame original, vamos a hacer una copia del mismo:
>>> df_mod.equals(df) # comprueba que todos los valores del DataFrame son iguales
True
>>> df_mod.head(1)
Revenue Employees City Country
Company
Apple 274515 147000 California United States
Supongamos que no se había contemplado una subida del 20% en los ingresos y queremos
reflejarla:
Supongamos que todas las empresas tecnológicas mueven su sede a Vigo (España) y
queremos reflejarlo:
>>> df_mod.head()
Revenue Employees City Country
Company
Apple 329418.0 137000 Vigo Spain
Samsung Electronics 240880.8 267937 Vigo Spain
Alphabet 219032.4 135301 Vigo Spain
Foxconn 218334.0 878429 Vigo Spain
Microsoft 171618.0 163000 Vigo Spain
Nota: En este último ejemplo se produce un «broadcast» o difusión del valor escalar a
todos los registros del «dataset».
Reemplazo de valores
>>> iso3166 = {
United States : USA ,
South Korea : KOR ,
Taiwan : TWN ,
China : CHN ,
Japan : JPN
}
Ejercicio
Recodifique la columna Province del «dataset» democan de tal manera que aparezcan las
provincias con el texto completo: Santa Cruz de Tenerife y Las Palmas de Gran Canaria.
>>> cisco
Revenue 51904
Employees 75900
City California
Country United States
Name: Cisco, dtype: object
>>> df_mod.tail(3)
Revenue Employees City Country
Company
HP Inc. 67966.8 53000 Vigo Spain
LG Electronics 64350.0 75000 Vigo Spain
Cisco 51904.0 75900 California United States
Truco: El método append() devuelve un nuevo DataFrame con los datos añadidos. Es por
eso que si queremos consolidar los cambios, debemos realizar una asignación.
4
Datos del año 2020 según Wikipedia.
>>> expenses
array([139655, 97509, 220777, 260609, 121145, 112338, 72815, 159843,
205695, 97672, 89614, 260028, 171650, 152049, 57006])
>>> df_mod.head()
Revenue Employees City Country Expenses
Company
Apple 329418.0 137000 Vigo Spain 139655
Samsung Electronics 240880.8 267937 Vigo Spain 97509
Alphabet 219032.4 135301 Vigo Spain 220777
Foxconn 218334.0 878429 Vigo Spain 260609
Microsoft 171618.0 163000 Vigo Spain 121145
Truco: También existe la función insert() que nos permite insertar una columna en una
posición determinada.
En el caso de que no nos haga falta una columna podemos borrarla fácilmente. Una opción
sería utilizar la función «built-in» del(), pero seguiremos con el uso de funciones propias de
pandas. Imaginemos que queremos eliminar la columna «Expenses»:
>>> df_mod.columns
Index([ Revenue , Employees , City , Country , Expenses ], dtype= object )
>>> df_mod.columns
Index([ Revenue , Employees , City , Country ], dtype= object )
Truco: Recordar que el parámetro axis indica en qué «dirección» estamos trabajando.
Véase el acceso a un DataFrame.
El parámetro inplace
>>> df_mod.head()
Revenue Employees City Country
Company
Apple 329418.0 137000 Vigo Spain
Samsung Electronics 240880.8 267937 Vigo Spain
Alphabet 219032.4 135301 Vigo Spain
Foxconn 218334.0 878429 Vigo Spain
Microsoft 171618.0 163000 Vigo Spain
>>> df_mod.head()
Revenue Employees
Company
Apple 329418.0 137000
Samsung Electronics 240880.8 267937
Alphabet 219032.4 135301
Foxconn 218334.0 878429
Microsoft 171618.0 163000
Ejercicio
Añada una nueva columna Density a democan de tal manera que represente la densidad de
población de cada isla del archipiélago canario.
>>> df.rename(columns=new_columns).head(3)
rev emp cit cou
(continué en la próxima página)
Otro camino para conseguir el mismo resultado es aplicar una función que realice esta tarea
de manera automatizada:
Ver también:
Si en vez del parámetro nominal columns utilizamos el parámetro index estaremos
renombrando los valores del índice. Se aplica el mismo comportamiento ya visto.
Nada impide asignar directamente una lista (tupla) de nombres a las columnas de
un DataFrame:
>>> df.head(3)
Ingresos Empleados Ciudad País
Company
Apple 274515 147000 California United States
Samsung Electronics 200734 267937 Suwon South Korea
Alphabet 182527 135301 California United States
A menudo solemos trabajar con datos que incluyen información textual. Pandas también nos
ofrece herramientas para cubrir estos casos.
De hecho, simplemente debemos utilizar el manejador str y tendremos a disposición la gran
mayoría de funciones vistas en la sección de cadenas de texto.
Veamos un primer ejemplo en el que pasamos a mayúsculas las ciudades en las que
se localizan las empresas tecnológicas:
Otro supuesto sería el de sustituir espacios por subguiones en los países de las
empresas:
Expresiones regulares
El uso de expresiones regulares aporta una gran expresividad. Veamos su aplicación con tres
casos de uso:
• Filtrado de filas.
• Reemplazo de valores.
• Extracción de columnas.
Supongamos que queremos filtrar las empresas y quedarnos con las que comienzan
por vocal:
>>> df[mask]
Revenue Employees City Country
Company
Apple 274515 147000 California United States
Alphabet 182527 135301 California United States
Intel 77867 110600 California United States
IBM 73620 364800 New York United States
Nota: Dado que el nombre de la empresa está actuando como índice del «dataset», hemos
aplicado la búsqueda sobre .index.
Ahora imaginemos que vamos a sustituir aquellas ciudades que empiezan con «S» o
«T» por «Stanton»:
Por último supongamos que queremos dividir la columna «Country» en dos columnas
usando el espacio como separador:
Existen otras funciones interesantes de Pandas que trabajan sobre expresiones regulares:
• count() para contar el número de ocurrencias de un patrón.
• contains() para comprobar si existe un determinado patrón.
• extract() para extraer grupos de captura sobre un patrón.
• findall() para encontrar todas las ocurrencias de un patrón.
Manejando fechas
Suele ser habitual tener que manejar datos en formato fecha (o fecha-hora). Pandas ofrece un
amplio abanico de posibilidades para ello. Veamos algunas de las herramientas disponibles.
Para ejemplificar este apartado hemos añadido al «dataset» de empresas tecnológicas una
nueva columna con las fechas de fundación de las empresas (en formato «string»):
>>> df[ Founded ] = [ 1/4/1976 , 13/1/1969 , 4/9/1998 , 20/2/1974 ,
... 4/4/1975 , 15/9/1987 , 1/2/1984 , 4/2/2004 ,
... 7/5/1946 , 1/10/1962 , 18/7/1968 , 16/6/1911 ,
... 11/11/1998 , 13/3/1918 , 1/11/1984 , 1/1/1939 ,
... 5/1/1947 ]
>>> df.head()
Revenue Employees City Country Founded
Company
Apple 274515 147000 California United States 1/4/1976
Samsung Electronics 200734 267937 Suwon South Korea 13/1/1969
Alphabet 182527 135301 California United States 4/9/1998
Foxconn 181945 878429 New Taipei City Taiwan 20/2/1974
Microsoft 143015 163000 Washington United States 4/4/1975
Por ejemplo, podríamos querer calcular el número de años que llevan activas las
empresas:
Los tipos de datos «datetime» dan mucha flexibilidad a la hora de hacer consultas:
Hay ocasiones en las que necesitamos que la fecha se convierta en el índice del DataFrame:
>>> df.head()
Company Revenue Employees City Country
Founded
1911-06-16 IBM 73620 364800 New York United States
1918-03-13 Panasonic 63191 243540 Osaka Japan
1939-01-01 HP Inc. 56639 53000 California United States
1946-07-05 Sony 84893 109700 Tokyo Japan
1947-05-01 LG Electronics 53625 75000 Seoul South Korea
Ejercicio
Partiendo del fichero oasis.csv que contiene información sobre la discografía del grupo de
pop británico Oasis, se pide:
• Cargue el fichero en un DataFrame.
• Convierta la columna «album_release_date» a tipo «datetime».
• Obtenga los nombres de los álbumes publicados entre 2000 y 2005.
Manejando categorías
Hasta ahora hemos visto tipos de datos numéricos, cadenas de texto y fechas. ¿Pero qué
ocurre con las categorías?
Las categorías pueden ser tanto datos numéricos como textuales, con la característica de
tener un número discreto (relativamente pequeño) de elementos y, en ciertas ocasiones, un
orden preestablecido. Ejemplos de variables categóricas son: género, idioma, meses del año,
color de ojos, nivel de estudios, grupo sanguíneo, valoración, etc.
Pandas facilita el tratamiento de datos categóricos mediante un tipo específico Categorical.
Siguiendo con el «dataset» de empresas tecnológicas, vamos a añadir el continente al que
pertenece cada empresa. En primera instancia mediante valores de texto habituales:
En este caso, al ser una conversión «automática», las categorías no han incluido ningún
tipo de orden. Pero imaginemos que queremos establecer un orden para las categorías de
continentes basadas, por ejemplo, en su población: Asia, África, Europa, América, Australia:
El hecho de trabajar con categorías ordenadas permite (entre otras) estas operaciones:
Vamos a aplicar las funciones estadísticas que proporciona pandas sobre la columna Revenue
de nuestro «dataset», aunque podríamos hacerlo sobre todas aquellas variables numéricas
susceptibles:
Ejercicio
Partiendo del conjunto de datos democan, obtenga aquellas islas cuya población está por
encima de la media del archipiélago canario.
Resultado esperado: [ Gran Canaria , Tenerife ]
Ordenando valores
Una operación muy típica cuando trabajamos con datos es la de ordenarlos en base a ciertos
criterios. Veamos cómo podemos hacerlo utilizando pandas. Volvemos a nuestro «dataset»
tecnológico:
>>> df
Revenue Employees City Country
(continué en la próxima página)
>>> df.sort_index()
Revenue Employees City Country
Company
Alphabet 182527 135301 California United States
Apple 274515 147000 California United States
Dell Technologies 92224 158000 Texas United States
Facebook 85965 58604 California United States
Foxconn 181945 878429 New Taipei City Taiwan
HP Inc. 56639 53000 California United States
Hitachi 82345 350864 Tokyo Japan
Huawei 129184 197000 Shenzhen China
IBM 73620 364800 New York United States
Intel 77867 110600 California United States
LG Electronics 53625 75000 Seoul South Korea
Lenovo 60742 71500 Hong Kong China
Microsoft 143015 163000 Washington United States
Panasonic 63191 243540 Osaka Japan
Samsung Electronics 200734 267937 Suwon South Korea
Sony 84893 109700 Tokyo Japan
Tencent 69864 85858 Shenzhen China
Ahora imaginemos que necesitamos tener las empresas ordenadas de mayor a menor
número de ingresos:
Al igual que veíamos en el caso de las series, podemos aplicar muchas de estas funciones de
máximos y mínimos sobre un DataFrame de Pandas.
Podemos obtener los valores mínimos y máximos de todas las columnas:
>>> df.min()
Revenue 53625
Employees 53000
City California
Country China
dtype: object
>>> df.max()
Revenue 274515
Employees 878429
City Washington
Country United States
dtype: object
También podría ser de utilidad saber qué empresa tiene el valor mínimo o máximo
para una determinada columna:
# LG tiene los menores ingresos
>>> df[ Revenue ].idxmin()
LG Electronics
Nota: En este caso nos devuelve una cadena de texto con el nombre de la empresa ya que
tenemos definido así nuestro índice (etiquetas). En otro caso devolvería la posición (numérica)
con un índice por defecto.
Si queremos acceder al registro completo, basta con acceder a través de la etiqueta devuelta:
>>> company = df[ Revenue ].idxmin()
>>> df.loc[company]
Revenue 53625
Employees 75000
City Seoul
Country South Korea
(continué en la próxima página)
Otra de las operaciones muy usuales es encontrar los 𝑛 registros con mayores/menores valores.
Supongamos que nos interesa conocer las 3 empresas con mayores ingresos y las 3
empresas con menor número de empleados/as:
Si queremos acceder al registro completo, podemos aplicar estas funciones de otro modo:
Ejercicio
Partiendo del conjunto de datos democan obtenga las 3 islas con menor densidad de población.
El resultado debería ser el siguiente:
>>> df
A B C
0 1 4.0 7.0
1 2 NaN 8.0
2 3 6.0 NaN
>>> df.isna()
A B C
0 False False False
1 False True False
2 False False True
Nota: También existe la función isnull() que funciona de manera análoga a isna(). En
StackExchange puedes ver una explicación de estas funciones.
En caso de que nos interese descartar los registros con valores nulos, procedemos así:
>>> df.dropna()
A B C
0 1 4.0 7.0
Sin embargo, también existe la posiblidad de rellenar los valores nulos con algún sustituto.
En este caso podemos ejecutar lo siguiente:
>>> df.fillna(0)
A B C
0 1 4.0 0.0
(continué en la próxima página)
>>> df.interpolate()
A B C
0 1 4.0 7.0
1 2 5.0 8.0
2 3 6.0 8.0
Reformando datos
En esta sección se verán las operaciones de pivotar y apilar que permiten reformar
(remodelar) un DataFrame.
Seguimos utilizando el conjunto de datos de empresas tecnológicas aunque nos quedaremos
únicamente con las 3 primeras filas a efectos didácticos:
>>> df = df.reset_index()[:3]
>>> df
Company Revenue Employees City Country
0 Apple 274515 147000 California United States
1 Samsung Electronics 200734 267937 Suwon South Korea
2 Alphabet 182527 135301 California United States
Ancho y Largo
Típicamente existen dos maneras de presentar datos tabulares: formato ancho y formato
largo. En formato ancho cada fila tiene múltiples columnas representando todas las
variables de una misma observación. En formato largo cada fila tiene básicamente tres
columnas: una que identifica la observación, otra que identifica la variable y otra que contiene
el valor.
Para pasar de formato ancho a formato largo usamos la función melt():
Truco: Nótese que las columnas tienen un nombre variable que se puede modificar
mediante columns.name.
Si queremos obtener el DataFrame en formato ancho, tal y como estaba, tenemos que realizar
alguna operación adicional: df.rename_axis(columns = None).reset_index().
Apilando datos
Las operaciones de apilado trabajan sobre los índices del DataFrame. Para comprobar su
aplicabilidad, vamos a añadir la columna «Company» como índice del «dataset» anterior:
>>> df
Revenue Employees City Country
Company
Apple 274515 147000 California United States
Samsung Electronics 200734 267937 Suwon South Korea
Alphabet 182527 135301 California United States
La función stack() nos permite obtener un DataFrame con índice multinivel que incluye
las columnas del DataFrame de origen y los valores agrupados:
>>> df_stacked
Company
Apple Revenue 274515
Employees 147000
City California
Country United States
Samsung Electronics Revenue 200734
Employees 267937
City Suwon
Country South Korea
Alphabet Revenue 182527
Employees 135301
City California
Country United States
dtype: object
>>> df_stacked.index
MultiIndex([( Apple , Revenue ),
( Apple , Employees ),
( Apple , City ),
( Apple , Country ),
( Samsung Electronics , Revenue ),
( Samsung Electronics , Employees ),
( Samsung Electronics , City ),
( Samsung Electronics , Country ),
( Alphabet , Revenue ),
( Alphabet , Employees ),
( Alphabet , City ),
( Alphabet , Country )],
names=[ Company , None])
La función unstack() realiza justo la operación contraria: convertir un DataFrame con índice
multinivel en un Dataframe en formato ancho con índice sencillo. Se podría ver como una
manera de aplanar el «dataset»:
>>> df_flat
Revenue Employees City Country
Company
Apple 274515 147000 California United States
Samsung Electronics 200734 267937 Suwon South Korea
Alphabet 182527 135301 California United States
Agrupando datos
Las operaciones de agregado son muy recurridas y nos permiten extraer información
relevante, que, a simple vista, quizás no sea tan evidente.
Veamos un ejemplo en el que calculamos la suma de los ingresos de las empresas,
agrupados por país:
Ver también:
Cuando realizamos una agrupación por varias columnas, el resultado contiene un índice de
múltiples niveles. Podemos aplanar el DataFrame usando unstack().
Incluso podemos aplicar distintas funciones de agregación a cada columna. Supongamos
que necesitamos calcular la media de los ingresos y la mediana del número de
Nota: Utilizamos la función agg() pasando un diccionario cuyas claves son nombres de
columnas y cuyos valores son funciones a aplicar.
Ejercicio
Obtenga el porcentaje de población (en relación con el total) de cada provincia de las Islas
Canarias en base al «dataset» democan.
El resultado debería ser similar a:
• Las Palmas de Gran Canaria: 52%
• Santa Cruz de Tenerife: 48%
Aplicando funciones
Pandas permite la aplicación de funciones (tanto propias como «built-in») a filas y/o
columnas de un DataFrame.
Numpy nos ofrece una amplia gama de funciones matemáticas. Podemos hacer uso de
cualquier de ellas aplicándola directamente a nuestro conjunto de datos. Veamos un ejemplo
en el que obtenemos el máximo de cada columna:
>>> df.apply(np.max)
Revenue 274515
Employees 878429
City Washington
Country United States
dtype: object
Podemos aplicar funciones sobre determinadas columnas. Supongamos que queremos obtener
el logaritmo de la serie de ingresos:
>>> df[ Revenue ].apply(np.log)
Company
Apple 12.522761
Samsung Electronics 12.209736
Alphabet 12.114653
Foxconn 12.111460
Microsoft 11.870705
Huawei 11.768993
Dell Technologies 11.431976
Facebook 11.361696
Sony 11.349147
Hitachi 11.318673
Intel 11.262758
IBM 11.206672
Tencent 11.154306
Panasonic 11.053917
Lenovo 11.014391
HP Inc. 10.944453
LG Electronics 10.889771
Name: Revenue, dtype: float64
Ahora ya podemos aplicar esta función a nuestro DataFrame, teniendo en cuenta que
debemos actuar sobre el eje de filas (axis=1):
>>> df.apply(raise_employment, axis=1)
Company
Apple 154350.00
Samsung Electronics 267937.00
Alphabet 142066.05
Foxconn 878429.00
Microsoft 171150.00
Huawei 197000.00
(continué en la próxima página)
El resultado es una serie que se podría incorporar al conjunto de datos, o bien, reemplazar
la columna Employees con estos valores.
Ejercicio
Supongamos que el Gobierno de Canarias va a dar unas ayudas a cada isla en función de su
superficie y su población, con las siguientes reglas:
• Islas con menos de 1000 km2 : ayuda del 30% de su población.
• Islas con más de 1000 km2 : ayuda del 20% de su población.
Añada una nueva columna Grant al «dataset» democan donde se contemplen estas ayudas.
El DataFrame debería quedar así:
Uniendo DataFrames
En esta sección veremos dos técnicas: Una de ellas «fusiona» dos DataFrames mientras que
la otra los «concatena».
Fusión de DataFrames
que la mezcla se dirija por determinadas columnas, tenemos a disposición los parámetros on,
left_on o right_on.
Ver también:
Existe la posibilidad de generar un producto cartesiano entre las filas de ambos DataFrames.
Para ello podemos usar pd.merge(df1, df2, how= cross ).
Concatenación de DataFrames
Para concatenar dos DataFrames podemos utilizar la función concat() que permite añadir
las filas de un DataFrame a otro, o bien añadir las columnas de un DataFrame a otro.
Ejercicio
Obtenga los datos de población y superficie de las comunidades autónomas españolas desde
esta url de Wikipedia en un único DataFrame con la siguiente estructura:
Notas:
8.4 matplotlib
La forma más común de importar esta librería es usar el alias plt de la siguiente manera:
1
Foto original de portada por Customerbox en Unsplash.
8.4.1 Figura
La figura es el elemento base sobre el que se construyen todos los gráficos en matplotlib.
Veamos cómo crearla:
>>> type(fig)
matplotlib.figure.Figure
>>> fig
<Figure size 640x480 with 0 Axes>
Podemos observar que la resolución (por defecto) de la figura es de 640x480 píxeles y que no
dispone de ningún eje («0 Axes»).
La resolución final de una figura viene determinada por su altura (height) y anchura (width)
especificadas en pulgadas2 que, a su vez, se multiplican por los puntos por pulgada o dpi.
Veamos el funcionamiento:
>>> fig
<Figure size 640x480 with 0 Axes>
Importante: Si utilizamos entornos de desarollo basados en Jupyter, los valores por defecto
son distintos:
• Ancho de figura: 6 in
• Alto de figura: 4 in
• DPI: 75
• Resolución: 450x300 px
Por tanto, cuando creamos una figura podemos modificar los parámetros por defecto para
obtener la resolución deseada:
Si nos interesa que cualquier figura tome unos valores concretos de resolución, podemos
modificar los valores por defecto del entorno. Para ello, matplotlib hace uso de un
diccionario plt.rcParams que contiene los parámetros globales de configuración. Veamos
cómo modificarlo:
>>> fig.get_figwidth()
10.0
>>> fig.get_figheight()
5.0
>>> fig.dpi
300.0
8.4.2 Marcos
>>> ax
<AxesSubplot:>
>>> fig
<Figure size 640x480 with 1 Axes>
Truco: Suele ser habitual encontrar ax como nombre de variable del «axes» devuelto por
la función add_subplot().
Nota: La escala por defecto de cada eje va de 0 a 1 con marcas cada 0.2
Ahora vamos a generar 4 marcos sobre los que fijaremos un título identificativo:
>>> fig
<Figure size 640x480 with 4 Axes>
Matplotlib nos ofrece una forma compacta de crear a la vez tanto la figura como los marcos
que necesitemos.
Para ello utilizaremos la función plt.subplots() que recibe como parámetros el número de
filas y el número de columnas para la disposición de los marcos, y devuelve una tupla con la
figura y los marcos.
En el siguiente ejemplo creamos una figura con un único marco:
>>> fig, ax = plt.subplots(1, 1)
>>> fig
<Figure size 640x480 with 1 Axes>
>>> ax
<AxesSubplot:>
único marco.
En el siguiente ejemplo creamos una figura con 6 marcos en disposición de 2 filas por 3
columnas:
>>> fig
<Figure size 640x480 with 6 Axes>
>>> ax
array([[<AxesSubplot:>, <AxesSubplot:>, <AxesSubplot:>],
[<AxesSubplot:>, <AxesSubplot:>, <AxesSubplot:>]], dtype=object)
>>> ax.shape
(2, 3)
Etiquetas
Dentro de un marco también es posible fijar las etiquetas de los ejes (X e Y). Veamos cómo
hacerlo:
>>> fig
<Figure size 640x480 with 1 Axes>
Ejes
Un marco (2D) está compuesto por dos ejes: eje X e eje Y. Podemos acceder a cada eje
mediante sendos atributos:
>>> ax.xaxis
<matplotlib.axis.XAxis at 0x112b34100>
>>> ax.yaxis
<matplotlib.axis.YAxis at 0x112b34850>
Rejilla
En cada eje podemos activar o desactivar la rejilla, así como indicar su estilo.
En primer lugar vamos a activar la rejilla en ambos ejes:
>>> ax.xaxis.grid(True)
>>> ax.yaxis.grid(True)
>>> ax.grid(True)
Truco: Las funciones de matplotlib que actúan como «interruptores» tienen por defecto el
valor verdadero. En este sentido ax.grid() invocada sin parámetros hace que se muestre la
rejilla. Esto se puede aplicar a muchas otras funciones.
Supongamos ahora que queremos personalizar la rejilla con estilos diferentes en cada eje:
Marcas
Por defecto, los ejes del marco tienen unas marcas3 equiespaciadas que constituyen las marcas
mayores. Igualmente existen unas marcas menores que, a priori, no están activadas.
Ambos elementos son susceptibles de modificarse. Veamos un ejemplo en el que establecemos
las marcas menores con distinto espaciado en cada eje y además le damos un estilo
diferente a cada rejilla:
3
Se suele usar el término inglés «ticks».
También es posible asignar etiquetas a las marcas menores. En ese sentido, veremos un
ejemplo en el que incorporamos los valores a los ejes con estilos propios:
• Marcas menores en el eje X: precisión de 1 decimal, tamaño de letra 8 y color gris.
• Marcas menores en el eje Y: precisión de 2 decimales, tamaño de letra 8 y color azul.
>>> # Eje X
>>> ax.xaxis.set_minor_formatter( {x:.1f} )
>>> ax.tick_params(axis= x , which= minor , labelsize=8, labelcolor= gray )
Vamos a empezar por representar la función 𝑓 (𝑥) = 𝑠𝑖𝑛(𝑥). Para ello crearemos una variable
𝑥 con valores flotantes equidistantes y una variable 𝑦 aplicando la función senoidal. Nos
apoyamos en numpy para ello. A continuación usaremos la función plot() del marco para
representar la función creada:
Múltiples funciones
Partiendo de un mismo marco, es posible graficar todas las funciones que necesitemos. A
continuación crearemos un marco con las funciones seno y coseno:
Nota: Los colores «auto» asignados a las funciones siguen un ciclo establecido por matplotlib
que es igualmente personalizable.
Leyenda
En el caso de que tengamos múltiples gráficos en el mismo marco puede ser deseable mostrar
una leyenda identificativa. Para usarla necesitamos asignar etiquetas a cada función. Veamos
a continuación cómo incorporar una leyenda:
>>> ax.legend()
<matplotlib.legend.Legend at 0x123c8f190>
Ubicación de la leyenda
Matplotlib intenta encontrar la mejor ubicación para la leyenda en el marco. Sin embargo,
también es posible personalizar el lugar en el que queremos colocarla.
Si nos interesa situar la leyenda en la parte superior central del marco haríamos lo
siguiente:
Aplicando estilos
Para cada función que incluimos en el marco es posible establecer un estilo personalizado con
multitud de parámetros. Veamos la aplicación de algunos de estos parámetros a las funciones
seno y coseno con las que hemos estado trabajando:
Acotando ejes
Hay veces que nos interesa definir los límites de los ejes. En ese caso, podemos hacerlo de
una manera muy sencilla:
Anotaciones
En ocasiones necesitamos añadir ciertas anotaciones al gráfico que estamos diseñando. Esto
permite destacar áreas o detalles que pueden ser relevantes.
Partiendo de las funciones seno y coseno con las que hemos estado trabajando, vamos a
suponer que queremos obtener sus puntos de corte, es decir, resolver la siguiente
ecuación:
𝑠𝑖𝑛(𝑥) = 𝑐𝑜𝑠(𝑥)
⇓
𝜋
𝑥 = + 𝜋𝑛, 𝑛 ∈ Z
4
Para el caso que nos ocupa haríamos 𝑛 = 0 con lo que obtendríamos la siguiente solución:
Ejercicio
Escriba el código Python necesario para obtener el siguiente gráfico:
Datos:
• 𝑥 ∈ [0, 2𝜋] (1000 puntos)
• 𝑦 = 𝑒−𝛼𝑥 𝑠𝑖𝑛(𝛽𝑥), donde 𝛼 = 0.7 y 𝛽 = 10.
Mediante matplotlib podemos hacer prácticamente cualquier tipo de gráfico. En esta sección
haremos un repaso por algunos de ellos.
Gráficos de barras
Vamos a partir de un «dataset» que contiene los resultados de los Juegos Olímpicos de Tokio
2020. Hemos descargado el fichero medals.xlsx desde una página de Kaggle4 .
En primer lugar cargaremos este fichero en un DataFrame y haremos una pequeña
«limpieza»:
>>> df.head()
Rank Team/NOC Gold Silver Bronze Total Rank by Total
0 1 United States of America 39 41 33 113 1
(continué en la próxima página)
4
Kaggle es un servicio web que ofrece una gran variedad de «datasets», así como código, cursos y otros
recursos en relación con la ciencia de datos.
>>> df.head()
Rank Gold Silver Bronze Total Rank by Total
Country
United States of America 1 39 41 33 113 1
People s Republic of China 2 38 32 18 88 2
Japan 3 27 14 17 58 5
Great Britain 4 22 21 22 65 4
ROC 5 20 28 23 71 3
>>> df_best
Rank Gold Silver Bronze Total Rank by Total
Country
United States of America 1 39 41 33 113 1
People s Republic of China 2 38 32 18 88 2
Japan 3 27 14 17 58 5
Great Britain 4 22 21 22 65 4
ROC 5 20 28 23 71 3
Australia 6 17 7 22 46 6
Netherlands 7 10 12 14 36 9
France 8 10 12 11 33 10
Germany 9 10 11 16 37 8
Italy 10 10 10 20 40 7
>>> ax.set_xticks(x)
>>> ax.set_xticklabels(df_best.index, rotation=90)
>>> ax.legend()
Ejercicio
Partiendo del fichero tiobe-2020-clean.csv que contiene las valoraciones de los lenguajes
de programación más usados durante el año 2020 (según el índice TIOBE)8 , cree el siguiente
gráfico de barras:
8
Datos extraídos desde esta página de Kaggle.
Gráficos de dispersión
Para este gráfico vamos a usar un «dataset» de jugadores de la NBA5 extraído desde esta
página de Kaggle. El fichero nba-data.csv contiene información desde 1996 hasta 2019.
En primer lugar cargamos los datos y nos quedamos con un subconjunto de las columnas:
>>> df.head()
pts reb ast
0 4.8 4.5 0.5
1 0.3 0.8 0.0
2 4.5 1.6 0.9
3 7.8 4.4 1.4
4 3.7 1.6 0.5
>>> df.shape
(11700, 3)
5
National Basketball League (liga estadounidense de baloncesto).
>>> p = ax.scatter(x, y,
... s=30, # tamaño de los puntos
... c=colors, cmap= RdBu_r , # colores
... vmin=colors.min(), vmax=colors.max(), # normalización de colores
... alpha=0.7,
... edgecolors= none )
>>> fig.tight_layout()
Ejercicio
Partiendo del fichero bmw-clean.csv que contiene información sobre vehículos de la marca
BMW9 , cree el siguiente gráfico de dispersión:
9
Datos extraídos desde esta página de Kaggle.
Histogramas
En esta ocasión vamos a trabajar con un «dataset» de «Avengers»6 extraído desde Kaggle.
Hemos descargado el fichero avengers.csv.
Como punto de partida vamos a cargar la información y a quedarnos únicamente con la
columna que hace referencia al año en el que se crearon los personajes:
>>> df.head()
Year
0 1963
1 1963
2 1963
3 1963
4 1963
>>> df.shape
(173, 1)
6
Los Vengadores son un equipo de superhéroes publicados por Marvel Comics.
Igualmente haremos un pequeño filtrado para manejar sólo registros a partir de 1960:
>>> df.shape
(159, 1)
>>> ax.hist(df,
... bins=bins, # intervalos de agrupación
... rwidth=0.95, # ancho de cada barra
... zorder=2, # barras por encima de rejilla
... color= deeppink ,
... alpha=0.5)
>>> fig.tight_layout()
Ejercicio
Partiendo del fichero pokemon.csv que contiene información sobre Pokemon10 , cree el
siguiente histograma en el que se analiza el número de personajes «pokemons» en función
de su velocidad (columna Speed):
10
Datos extraídos desde esta página de Kaggle.
Vamos a trabajar con un conjunto de datos extraído desde esta página de Kaggle que contiene
información histórica de temperaturas del planeta Tierra. El fichero global-temperatures.
csv se ha descargado para su tratamiento.
En primer lugar cargamos los datos, renombramos las columnas y eliminamos los valores
nulos:
>>> df = pd.read_csv( pypi/datascience/files/global-temperatures.csv ,
... parse_dates=[ dt ], # conversión a tipo datetime
... usecols=[ dt , LandAverageTemperature ])
>>> df.head()
when temp
0 1750-01-01 3.034
1 1750-02-01 3.083
2 1750-03-01 5.626
3 1750-04-01 8.490
4 1750-05-01 11.573
(continué en la próxima página)
>>> df.shape
(3180, 2)
>>> ax.plot(x, y,
... linestyle= None , marker= . , color= tomato , # estilo de línea
... zorder=2) # orden para colocar sobre rejilla
>>> fig.tight_layout()
Mapas de calor
Para este tipo de gráfico vamos a utilizar un «dataset» que recoge las 1000 películas más
valoradas en IMDB7 . Está sacado desde esta página de Kaggle y se ha descargado el fichero
de datos en imdb-top-1000.csv.
En primer lugar vamos a cargar los datos quedándonos con las columnas Certificate
(clasificación de la película según edades), Genre (géneros de la película) e IMDB_Rating
(valoración de la película en IMDB):
>>> df.head()
Certificate Genre IMDB_Rating
0 A Drama 9.3
1 A Crime, Drama 9.2
2 UA Action, Crime, Drama 9.0
3 A Crime, Drama 9.0
4 U Crime, Drama 9.0
Ahora creamos una nueva columna en el DataFrame donde guardaremos únicamente el género
principal de cada película:
>>> ratings
Main_Genre Animation Action Adventure Biography Comedy Crime Drama
Certificate
ALL 7.947368 8.165000 7.953571 7.862500 7.940541 8.200000 7.976364
>12 7.883333 7.992424 7.958333 7.971429 7.885714 7.900000 7.953659
>13 7.866667 7.783333 7.600000 7.862500 7.785714 8.000000 7.775000
>17 7.800000 7.812500 7.900000 7.900000 7.824138 7.814286 7.915094
>18 7.866667 7.873171 7.912500 8.017647 7.877778 8.130233 8.036364
>>> x = ratings.columns
(continué en la próxima página)
>>> # Mostrar las etiquetas. El color del texto cambia en función de su normalización
>>> for i in range(len(y)):
... for j in range(len(x)):
... value = ratings.iloc[i, j]
... text_color = text_colors[int(im.norm(value) > 0.5)] # color etiqueta
... ax.text(j, i, f {value:.2f} , color=text_color, va= center , ha= center )
>>> ax.spines[:].set_visible(False)
>>> fig.tight_layout()
Ejercicio
Partiendo del fichero euro-dollar-clean.csv que contiene información sobre el cambio
euro-dollar durante los últimos 12 años11 , cree el siguiente mapa de calor en el que se analiza
la evolución del cambio enfrentando meses y años:
Diagramas de caja
11
Datos extraídos desde esta página de Kaggle.
14
Inspirado en este artículo de Towards Data Science.
Para mostrar el funcionamiento de los diagramas de caja en Matplotlib vamos a hacer uso
de distintas distribuciones aleatorias que crearemos mediante funciones de Numpy:
>>> DIST_SIZE = 100 # tamaño de la muestra
>>> boxplots = []
>>> boxplots.append(dict(
... dist=np.random.normal(0, 1, size=DIST_SIZE),
... label= Normal\n$\mu=0, \sigma=1$ ,
... fill_color= pink ,
... brush_color= deeppink ))
>>> boxplots.append(dict(
... dist=np.random.geometric(0.4, size=DIST_SIZE),
... label= Geometric\n$p=0.4$ ,
... fill_color= lightblue ,
... brush_color= navy ))
>>> boxplots.append(dict(
... dist=np.random.chisquare(2, size=DIST_SIZE),
... label= Chi-squared\n$df=2$ ,
... fill_color= lightgreen ,
... brush_color= darkgreen ))
Ahora ya podemos construir el gráfico de cajas que nos permite visualizar la distribución de
las muestras:
>>> fig, ax = plt.subplots(figsize=(8, 6), dpi=100) # 800x600 px
>>> ax.spines[:].set_visible(False)
>>> fig.tight_layout()
Gráficos de evolución
>>> df_smooth.head()
Open Volume
Date
2017-01-21 9.968611 2.146882
2017-01-22 10.105573 2.117377
2017-01-23 10.222339 1.985587
2017-01-24 10.273270 1.821968
2017-01-25 10.239854 1.647938
>>> # Rejilla
>>> ax.xaxis.grid(color= lightgray , linewidth=.5)
>>> for y_tick in y_ticks:
... if y_tick != 0:
... ax.axhline(y_tick, color= lightgray , linewidth=.5)
>>> ax.legend()
>>> ax.spines[:].set_visible(False)
>>> fig.tight_layout()
Ejercicio
Partiendo del fichero mwh-spain-2021-clean.csv que contiene información sobre el precio
de la energía en España durante el año 202112 , cree el siguiente diagrama de evolución que
representa la variación del precio del MWh13 en función del tiempo:
12
Datos extraídos desde esta página de El País.
13
Mega Watio Hora (medida de consumo de energía)
Scraping
Si bien existen multitud de datos estructurados en forma de ficheros, hay otros muchos que
están embebidos en páginas web y que están preparados para ser visualizados mediante un
navegador.
Sin embargo, las técnicas de «scraping» nos permiten extraer este tipo de información
web para convertirla en datos estructurados con los que poder trabajar de forma más
cómoda.
Los paquetes que veremos en este capítulo bien podrían estar incluidos en otras temáticas,
ya que no sólo se utilizan para «scraping».
449
Aprende Python
9.1 requests
El paquete requests es uno de los paquetes más famosos del ecosistema Python. Como dice
su lema «HTTP for Humans» permite realizar peticiones HTTP de una forma muy sencilla
y realmente potente.1
Hemos ejecutado una solicitud GET al sitio web https://pypi.org. La respuesta se almacena
en un objeto de tipo requests.models.Response muy rica en métodos y atributos que
veremos a continuación:
>>> type(response)
requests.models.Response
1
Foto original de portada por Frame Harirak en Unsplash.
Quizás lo primero que nos interese sea ver el contenido de la respuesta. En este sentido
requests nos provee del atributo text que contendrá el contenido html del sitio web en
cuestión como cadena de texto:
>>> response.text
\n\n\n\n\n\n<!DOCTYPE html>\n<html lang="en" dir="ltr">\n <head>\n <meta␣
˓→charset="utf-8">\n <meta http-equiv="X-UA-Compatible" content="IE=edge">\n
˓→<meta name="viewport" content="width=device-width, initial-scale=1">\n\n <meta␣
˓→name="defaultLanguage" content="en">\n <meta name="availableLanguages" content=
˓→"en, es, fr, ja, pt_BR, uk, el, de, zh_Hans, zh_Hant, ru, he, eo">\n\n \n\n
˓→<title>PyPI · The Python Package Index</title>\n <meta name="description"␣
˓→content="The Python Package Index (PyPI) is a repository of software for the␣
˓→400italic,600,600italic,700,700italic%7CSource+Code+Pro:500">\n <noscript>\n ␣
˓→ <link rel="stylesheet" href="/static/css/noscript.d4ce1e76.css">\n
>>> response.status_code
200
Truco: Para evitar la comparación directa con el literal 200, existe la variable requests.
codes.ok.
Con requests podemos realizar peticiones mediante cualquier método HTTP2 . Para ello,
simplemente usamos el método correspondiente del paquete:
2
Métodos de petición HTTP.
Cuando se realiza una petición HTTP es posible incluir parámetros. Veamos distintas
opciones que nos ofrece requests para ello.
Query string
En una petición GET podemos incluir parámetros en el llamado «query string». Los
parámetros se definen mediante un diccionario con nombre y valor de parámetro.
Veamos un ejemplo sencillo. Supongamos que queremos buscar paquetes de Python que
contengan la palabra «astro»:
>>> response.url
https://pypi.org/?q=astro
Truco: El atributo url nos devuelve la URL a la se ha accedido. Útil en el caso de paso de
parámetros.
Parámetros POST
Una petición POST, por lo general, siempre va acompañada de una serie de parámetros que
típicamente podemos encontrar en un formulario web. Es posible realizar estas peticiones en
requests adjuntando los parámetros que necesitemos en el mismo formato de diccionario que
hemos visto para «query string».
Supongamos un ejemplo en el que tratamos de logearnos en la página de GIPHY con
nombre de usuario y contraseña. Para ello, lo primero que debemos hacer es inspeccionar3
los elementos del formulario e identificar los nombres («name») de los campos. En este caso
los campos son email y password:
Hemos obtenido un código de estado 403 indicando que el acceso está prohibido.
Envío de cabeceras
Hay veces que necesitamos modificar o añadir determinados campos en las cabeceras4 de la
petición. Su tratamiento también se realiza a base de diccionarios que son pasados al método
correspondiente.
Uno de los usos más típicos de las cabeceras es el «user agent»5 donde se especifica el tipo
de navegador que realiza la petición. Supongamos un ejemplo en el que queremos especificar
que el navegador corresponde con un Google Chrome corriendo sobre Windows
10:
>>> response.status_code
200
3
Herramientas para desarrolladores en el navegador. Por ejemplo Chrome Dev Tools.
4
Las cabeceras HTTP permiten al cliente y al servidor enviar información adicional junto a una petición
o respuesta.
5
El agente de usuario del navegador permite que el servidor identifique el sistema operativo y las
características del navegador.
Contenido JSON
>>> response.url
https://api.open-meteo.com/v1/forecast?latitude=28.4578025&longitude=-16.3563748&
˓→hourly=temperature_2m
>>> type(data)
dict
>>> data.keys()
dict_keys([ utc_offset_seconds , elevation , latitude , hourly_units , longitude
˓→ , generationtime_ms , hourly ])
Ahora podríamos mostrar la predicción de temperatures de una manera algo más visual.
Según la documentación de la API sabemos que la respuesta contiene 168 medidas de
temperatura correspondientes a todas las horas durante 7 días. Supongamos que sólo
queremos mostrar la predicción de temperaturas hora a hora para el día de
mañana:
Cabeceras de respuesta
Tras una petición HTTP es posible recuperar las cabeceras que vienen en la respuesta a
través del atributo headers como un diccionario:
Cookies
>>> response.cookies.keys()
[ _octo , logged_in , _gh_sess ]
6
Una cookie HTTP es una pequeña pieza de datos que un servidor envía al navegador web del usuario.
Ejercicio
Utilizando el paquete requests, haga una petición GET a https://twitter.com y obtenga los
siguientes campos:
• Código de estado.
• Longitud de la respuesta.
• Valor de la cookie guest_id
• Valor de la cabecera content-encoding
Hay ocasiones en las que usamos requests para descargar un fichero, bien sea en texto plano
o binario. Veamos cómo proceder para cada tipo.
>>> response.status_code
200
Consejo: Usamos response.text para obtener el contenido ya que nos interesa en formato
«unicode».
$ file data.csv
plain_text.csv: UTF-8 Unicode text, with CRLF line terminators
Ficheros binarios
Para descargar ficheros binarios seguimos la misma estructura que para ficheros en texto
plano, pero indicando el tipo binario a la hora de escribir en disco:
>>> response.status_code
200
$ file data.xlsx
data.xlsx: Microsoft OOXML
Nombre de fichero
En los ejemplos anteriores hemos puesto el nombre de fichero «a mano». Pero podría
darse la situación de necesitar el nombre de fichero que descargamos. Para ello existen dos
aproximaciones en función de si aparece o no la clave «attachment» en las cabeceras de
respuesta.
Podemos escribir la siguiente función para ello:
>>> get_filename(response)
pytest.pdf
>>> get_filename(response)
45070.csv
9.2 beautifulsoup
Para empezar a trabajar con Beautiful Soup es necesario construir un objeto de tipo
BeautifulSoup que reciba el contenido a «parsear»:
>>> contents =
... <html lang="en">
... <head>
... <title>Just testing</title>
... </head>
... <body>
... <h1>Just testing</h1>
... <div class="block">
... <h2>Some links</h2>
... <p>Hi there!</p>
... <ul id="data">
... <li class="blue"><a href="https://example1.com">Example 1</a></li>
... <li class="red"><a href="https://example2.com">Example 2</a></li>
... <li class="gold"><a href="https://example3.com">Example 3</a></li>
... </ul>
... </div>
... <div class="block">
... <h2>Formulario</h2>
... <form action="" method="post">
... <label for="POST-name">Nombre:</label>
... <input id="POST-name" type="text" name="name">
... <input type="submit" value="Save">
... </form>
... </div>
... <div class="footer">
... This is the footer
... <span class="inline"><p>This is span 1</p></span>
... <span class="inline"><p>This is span 2</p></span>
... <span class="inline"><p>This is span 2</p></span>
... </div>
... </body>
(continué en la próxima página)
2
Analizar y convertir una entrada en un formato interno que el entorno de ejecución pueda realmente
manejar.
1
Foto original de portada por Ella Olsson en Unsplash.
Atención: Importar el paquete usando bs4. Suele llevar a equívoco por el nombre
original.
A partir de aquí se abre un abanico de posibilidades que iremos desgranando en los próximos
epígrafes.
Fórmulas de localización
>>> soup.find_all( a )
[<a href="https://example1.com">Example 1</a>,
<a href="https://example2.com">Example 2</a>,
<a href="https://example3.com">Example 3</a>]
• Localizar todos los elementos cuyo atributo type tenga el valor text:
• Localizar todos los elementos de título h1, h2, h3, .... Esto lo podemos atacar
usando expresiones regulares:
• Localizar todos los párrafos que están dentro del pie de página (usando selectores
CSS):
Hasta ahora hemos visto las funciones find_all() y select() que localizan un conjunto de
elementos. Incluso en el caso de encontrar sólo un elemento, se devuelve una lista con ese
único elemento.
Beautiful Soup nos proporciona la función find() que trata de localizar un único
elemento. Hay que tener en cuenta dos circunstancias:
• En caso de que el elemento buscado no exista, se devuelve None.
• En caso de que existan múltiples elementos, se devuelve el primero.
Veamos algunos ejemplos de esto:
Todas las búsquedas se pueden realizar desde cualquier elemento preexistente, no únicamente
desde la raíz del DOM.
Veamos un ejemplo de ello. Si tratamos de localizar todos los títulos «h2» vamos a
encontrar dos de ellos:
>>> soup.find_all( h2 )
[<h2>Some links</h2>, <h2>Formulario</h2>]
Pero si, previamente, nos ubicamos en el segundo bloque de contenido, sólo vamos a encontrar
uno de ellos:
>>> second_block
<div class="block">
<h2>Formulario</h2>
<form action="" method="post">
<label for="POST-name">Nombre:</label>
<input id="POST-name" name="name" type="text"/>
<input type="submit" value="Save"/>
</form>
</div>
>>> second_block.find_all( h2 )
[<h2>Formulario</h2>]
Hay definidas una serie de funciones adicionales de búsqueda para cuestiones más
particulares:
• Localizar los «div» superiores a partir de un elemento concreto:
>>> blue_li.find_next_siblings()
[<li class="red"><a href="https://example2.com">Example 2</a></li>,
<li class="gold"><a href="https://example3.com">Example 3</a></li>]
Al igual que en las anteriores, es posible aplicar un filtro al usar esta función.
También existe la versión de esta función que devuelve un único elemento:
find_next_sibling().
• Localizar los elementos hermanos anteriores a uno dado:
>>> gold_li.find_previous_siblings()
[<li class="red"><a href="https://example2.com">Example 2</a></li>,
<li class="blue"><a href="https://example1.com">Example 1</a></li>]
Al igual que en las anteriores, es posible aplicar un filtro al usar esta función.
También existe la versión de esta función que devuelve un único elemento:
find_previous_sibling().
• Localizar todos los elementos a continuación de uno dado:
>>> submit.find_all_next()
[<div class="footer">
This is the footer
<span class="inline"><p>This is span 1</p></span>
<span class="inline"><p>This is span 2</p></span>
<span class="inline"><p>This is span 2</p></span>
</div>,
<span class="inline"><p>This is span 1</p></span>,
<p>This is span 1</p>,
<span class="inline"><p>This is span 2</p></span>,
<p>This is span 2</p>,
<span class="inline"><p>This is span 2</p></span>,
<p>This is span 2</p>]
Al igual que en las anteriores, es posible aplicar un filtro al usar esta función.
También existe la versión de esta función que devuelve un único elemento: find_next().
• Localizar todos los elementos previos a uno dado:
>>> ul_data.find_all_previous([ h1 , h2 ])
[<h2>Some links</h2>, <h1>Just testing</h1>]
>>> ul_data.find_parents([ h1 , h2 ])
[]
Aunque uno de los preceptos del Zen de Python es «Explicit is better than implicit», el uso
de estos atajos puede estar justificado en función de muchas circunstancias.
Simplificando, podríamos decir que cada elemento de la famosa «sopa» de Beautiful Soup
puede ser un bs4.element.Tag o un «string».
Para el caso de los «tags» existe la posibilidad de acceder a su contenido, al nombre del
elemento o a sus atributos.
Nombre de etiqueta
>>> soup.name
[document]
Truco: Es posible modificar el nombre de una etiqueta con una simple asignación.
Acceso a atributos
>>> elem
<input id="POST-name" name="name" type="text"/>
>>> elem[ id ]
POST-name
>>> elem.attrs
{ id : POST-name , type : text , name : name }
Contenido textual
Es necesario aclarar las distintas opciones que proporciona Beautiful Soup para acceder al
contenido textual de los elementos del DOM.
Atributo Devuelve
text Una cadena de texto con todos los contenidos textuales del elemento
incluyendo espacios y saltos de línea
strings Un generador de todos los contenidos textuales del elemento incluyendo
espacios y saltos de línea
Un generador de todos los contenidos textuales del elemento eliminando
stripped_strings
espacios y saltos de línea
string Una cadena de texto con el contenido del elemento, siempre que contenga
un único elemento textual
Ejemplos:
>>> footer.text
\n This is the footer\n This is span 1\nThis is span 2\nThis is span 2\n
>>> list(footer.strings)
[ \n This is the footer\n ,
This is span 1 ,
\n ,
This is span 2 ,
\n ,
This is span 2 ,
\n ]
>>> list(footer.stripped_strings)
[ This is the footer , This is span 1 , This is span 2 , This is span 2 ]
Mostrando elementos
Cualquier elemento del DOM que seleccionemos mediante este paquete se representa con el
código HTML que contiene. Por ejemplo:
>>> data
<ul id="data">
<li class="blue"><a href="https://example1.com">Example 1</a></li>
<li class="red"><a href="https://example2.com">Example 2</a></li>
<li class="gold"><a href="https://example3.com">Example 3</a></li>
</ul>
>>> print(pretty_data)
<ul id="data">
<li class="blue">
<a href="https://example1.com">
Example 1
</a>
</li>
<li class="red">
<a href="https://example2.com">
Example 2
</a>
</li>
<li class="gold">
<a href="https://example3.com">
Example 3
</a>
</li>
</ul>
Además de localizar elementos, este paquete permite moverse por los elementos del DOM de
manera muy sencilla.
Para ir profundizando en el DOM podemos utilizar los nombres de los «tags» como
atributos del objeto, teniendo en cuenta que si existen múltiples elementos sólo se accederá
al primero de ellos:
>>> soup.div.p
<p>Hi there!</p>
>>> soup.form.label
<label for="POST-name">Nombre:</label>
>>> type(soup.span)
bs4.element.Tag
>>> type(soup.form) # todos los elementos del DOM son de este tipo
bs4.element.Tag
>>> soup.form.contents
[ \n ,
<label for="POST-name">Nombre:</label>,
\n ,
<input id="POST-name" name="name" type="text"/>,
\n ,
<input type="submit" value="Save"/>,
\n ]
>>> type(soup.form.contents)
list
>>> soup.form.children
<list_iterator at 0x106643100>
>>> block.contents
[ \n ,
<h2>Formulario</h2>,
\n ,
<form action="" method="post">
<label for="POST-name">Nombre:</label>
<input id="POST-name" name="name" type="text"/>
<input type="submit" value="Save"/>
</form>,
\n ]
>>> block.descendants
<generator object Tag.descendants at 0x10675d200>
>>> list(block.descendants)
[ \n ,
<h2>Formulario</h2>,
Formulario ,
\n ,
<form action="" method="post">
<label for="POST-name">Nombre:</label>
<input id="POST-name" name="name" type="text"/>
<input type="submit" value="Save"/>
</form>,
\n ,
<label for="POST-name">Nombre:</label>,
(continué en la próxima página)
Para acceder al elemento superior de otro dado, podemos usar el atributo parent:
>>> li.parent
<ul id="data">
<li class="blue"><a href="https://example1.com">Example 1</a></li>
<li class="red"><a href="https://example2.com">Example 2</a></li>
<li class="gold"><a href="https://example3.com">Example 3</a></li>
</ul>
Otros movimientos
En la siguiente tabla se recogen el resto de atributos que nos permiten movernos a partir de
un elemento del DOM:
Atributo Descripción
next_sibling Obtiene el siguiente elemento «hermano»
previous_sibling Obtiene el anterior elemento «hermano»
next_siblings Obtiene los siguientes elementos «hermanos» (iterador)
previous_siblings Obtiene los anteriores elementos «hermanos» (iterador)
next_element Obtiene el siguiente elemento
previous_element Obtiene el anterior elemento
Advertencia: Con estos accesos también se devuelven los saltos de línea \n como
elementos válidos. Si se quieren evitar, es preferible usar las funciones definidas aquí .
Ejercicio
Escriba un programa en Python que obtenga de https://pypi.org datos estructurados de los
«Trending projects» y los muestre por pantalla utilizando el siguiente formato:
<nombre-del-paquete>,<versión>,<descripción>,<url>
Se recomienda usar el paquete requests para obtener el código fuente de la página. Hay
que tener en cuenta que el listado de paquetes cambia cada pocos segundos, a efectos de
comprobación.
9.3 selenium
Documentación
Prerequisitos
Navegador web
Selenium necesita un navegador web instalado en el sistema para poder funcionar. Dentro
de las opciones disponibles están Chrome, Firefox, Edge, Internet Explorer y Safari. En el
caso de este documento vamos a utilizar Firefox. Su descarga e instalación es muy sencilla.
Driver
El «driver» es el manejador de las peticiones del usuario. Se trata del objeto fundamental
en Selenium que nos permitirá interactuar con el navegador y los sitios web.
Truco: Es posible usar otros navegadores. La elección de este documento por Firefox tiene
que ver con cuestiones de uso durante los últimos años.
Una de las opciones más utilizadas es la capacidad de ocultar la ventana del navegador. Esto
es útil cuando ya hemos probado que todo funciona y queremos automatizar la tarea:
>>> options=Options()
>>> options.profile = firefox_profile
• https://searchfox.org/mozilla-release/source/browser/app/profile/firefox.js
Fichero de log
Desde la primera vez que inicializamos el «driver», se crea un fichero de log en el directorio
de trabajo con el nombre geckodriver.log3 . En este fichero se registran todos los mensajes
que se producen durante el tiempo de vida del navegador.
Es posible, aunque no recomendable, evitar que se cree el fichero de log de la siguiente
manera:
>>> import os
Nota: De igual modo, con el método anterior podemos cambiar la ruta y el nombre del
fichero de log.
9.3.3 Navegando
Importante: Cuando se navega a un sitio web, Selenium espera (por defecto) a que la
propiedad document.readyState tenga el valor complete. Esto no implica necesariamente
que la página se haya cargado completamente, especialmente en páginas que usan mucho
javascript para cargar contenido dinámicamente.
>>> driver.forward()
>>> driver.back()
>>> driver.refresh()
Una vez terminadas todas las operaciones requeridas, es altamente recomendable salir del
navegador para liberar los recursos que pueda estar utilizando:
>>> driver.quit()
Una vez que hemos accedido a un sitio web, estamos en disposición de localizar elementos
dentro del DOM4 . El objeto driver nos ofrece las siguientes funciones para ello:
Todas estas funciones tienen su correspondiente versión para devolver un único elemento
que cumpla con el filtro especificado. En caso de que hayan varios, sólo se devolverá el primero
de ellos. El nombre de estas funciones sigue el patrón en singular:
find_element_by_<accesor>()
Veamos un ejemplo práctico de esto. Supongamos que queremos obtener los epígrafes
principales de la tabla de contenidos de «Aprende Python»:
Truco: Un poco más adelante veremos la forma de acceder a la información que contiene
cada elemento del DOM.
>>> type(element)
selenium.webdriver.firefox.webelement.FirefoxWebElement
9.3.5 Interacciones
Si bien el acceso a la información de un sitio web puede ser un objetivo en sí mismo, para
ello podríamos usar herramientas como requests. Sin embargo, cuando entra en juego la
interacción con los elementos del DOM, necesitamos otro tipo de aproximaciones.
Selenium nos permite hacer clic en el lugar deseado, enviar texto por teclado, borrar una
caja de entrada o manejar elementos de selección, entre otros.
Clic
Para hacer clic utilizamos la función homónima. Veamos un ejemplo en el que accedemos
a https://amazon.es y tenemos que aceptar las «cookies» haciendo clic en el botón
correspondiente:
>>> accept_cookies.click()
Inspeccionando el DOM
Para el ejemplo anterior de Amazon en el que debemos identificar el botón para aceptar
«cookies», abrimos el inspector de Firefox y descubrimos que su id es sp-cc-accept. Si
no lo tuviéramos disponible habría que hacer uso de otros localizadores como «xpath» o
selectores de estilo.
Ejercicio
5
Para Firefox tenemos a disposición la herramienta Inspector.
Enviar texto
Típicamente encontraremos situaciones donde habrá que enviar texto a algún campo de
entrada de un sitio web. Selenium nos permite hacer esto.
Veamos un ejemplo en el que tratamos de hacer login sobre PyPI:
>>> login_btn.click()
En el caso de que queramos enviar alguna tecla «especial», Selenium nos proporciona un
conjunto de símbolos para ello, definidos en selenium.webdriver.common.keys.
Por ejemplo, si quisiéramos enviar las teclas de cursor, haríamos lo siguiente:
>>> element.send_keys(Keys.RIGHT) # →
>>> element.send_keys(Keys.DOWN) # ↓
>>> element.send_keys(Keys.LEFT) # ←
>>> element.send_keys(Keys.UP) # ↑
Ejercicio
Escriba un programa en Python utilizando Selenium que, dada una palabra de 5 caracteres,
permita enviar ese «string» a https://wordle.danielfrg.com/ para jugar.
Tenga en cuenta lo siguiente:
• En primer lugar hay que pulsar el botón de «¡JUGAR!».
Borrar contenido
Si queremos borrar el contenido de un elemento web editable, típicamente una caja de texto,
lo podemos hacer usando el método .clear().
Manejo de selects
Si queremos identificar las opciones que están actualmente seleccionadas, podemos usar los
siguientes atributos:
>>> lang_handler.all_selected_options
[<selenium.webdriver.firefox.webelement.FirefoxWebElement (session="8612e5b7-6e66-
˓→4121-8869-ffce4139d197", element="8433ffdb-a8ad-4e0e-9367-d63fe1418b94")>]
>>> lang_handler.first_selected_option
<selenium.webdriver.firefox.webelement.FirefoxWebElement (session="8612e5b7-6e66-
˓→4121-8869-ffce4139d197", element="8433ffdb-a8ad-4e0e-9367-d63fe1418b94")>
>>> lang_handler.options
[..., ..., ...]
Ver también:
API del objeto Select
Como ya hemos comentado, los objetos del DOM con los que trabaja Selenium son de tipo
FirefoxWebElement. Veremos los mecanismos disponibles para poder acceder a sus atributos.
Para ejemplificar el acceso a la información de los elementos del DOM, vamos a localizar
el botón de descarga en la web de Ubuntu:
Nombre de etiqueta
>>> download_btn.tag_name
a
Tamaño y posición
Para cada elemento podemos obtener un diccionario que contiene la posición en pantalla
(𝑥, 𝑦) acompañado del ancho y del alto (todo en píxeles):
>>> download_btn.rect
{ x : 120.0,
y : 442.3999938964844,
width : 143.64999389648438,
height : 36.80000305175781}
Estado
Veamos las funciones disponibles para saber si un elemento se está mostrando, está habilitado
o está seleccionado:
>>> download_btn.is_displayed()
True
>>> download_btn.is_enabled()
True
>>> download_btn.is_selected()
False
Propiedad CSS
Elemento superior
Selenium también permite obtener el elemento superior que contiene a otro elemento dado:
>>> download_btn.parent
<selenium.webdriver.firefox.webdriver.WebDriver (session="8612e5b7-6e66-4121-8869-
˓→ffce4139d197")>
Propiedad de elemento
De manera más genérica, podemos obtener el valor de cualquier atributo de un elemento del
DOM a través de la siguiente función:
>>> download_btn.get_attribute( href )
https://ubuntu.com/engage/developer-desktop-productivity-whitepaper
9.3.7 Esperas
Cuando navegamos a un sitio web utilizando driver.get() es posible que el elemento que
estamos buscando no esté aún cargado en el DOM porque existan peticiones asíncronas
pendientes o contenido dinámico javascript. Es por ello que Selenium pone a nuestra
disposición una serie de esperas explícitas hasta que se cumpla una determinada condición.
Las esperas explícitas suelen hacer uso de condiciones de espera. Cada una de estas funciones
se puede utilizar para un propósito específico. Quizás una de las funciones más habituales
sea presence_of_element_located().
Veamos un ejemplo en el que cargamos la web de Stack Overflow y esperamos a que el pie
de página esté disponible:
>>> from selenium.webdriver.support.ui import WebDriverWait
>>> from selenium.webdriver.support import expected_conditions as EC
>>> from selenium.webdriver.common.by import By
>>> print(footer.text)
STACK OVERFLOW
Questions
Jobs
Developer Jobs Directory
Salary Calculator
Help
Mobile
...
Puede llegar a ser muy útil la ejecución de javascript en el navegador. La casuística es muy
variada. En cualquier caso, Selenium nos proporciona el método execute_script() para
esta tarea.
Supongamos un ejemplo en el que queremos navegar a la web de GitHub y hacer
«scroll» hasta el final de la página:
Ejercicio
Escriba un programa en Python que permita sacar un listado de supermercados Mercadona
dada una geolocalización (lat,lon) como dato de entrada.
Pasos a seguir:
1. Utilizar el siguiente f-string para obtener la url de acceso: f https://info.mercadona.
es/es/supermercados?coord={lat}%2C{lon}
2. Aceptar las cookies al acceder al sitio web.
3. Hacer scroll hasta el final de la página para hacer visible el botón «Ver todos». Se
recomienda usar javascript para ello.
4. Localizar el botón «Ver todos» y hacer clic para mostrar todos los establecimientos
(de la geolocalización). Se recomienda una espera explícita con acceso por «xpath».
5. Recorrer los elementos desplegados li y mostrar el contenido textual de los elementos
h3 que hay en su interior.
Como detalle final, y una vez que compruebe que el programa funciona correctamente,
aproveche para inicializar el «driver» ocultando la ventana del navegador.
Puede probar su programa con la localización de Las Palmas de Gran Canaria (28.1035677,
-15.5319742).