[go: up one dir, main page]

0% encontró este documento útil (0 votos)
497 vistas360 páginas

Guía Completa de Python para Principiantes

El documento es una guía completa sobre Python, que abarca desde la instalación hasta conceptos avanzados, dividida en 9 secciones que incluyen ejemplos prácticos y errores comunes. Se destaca la versatilidad de Python en diversas aplicaciones, como inteligencia artificial, desarrollo web y análisis de datos, y se menciona la importancia de su comunidad. Además, se ofrecen alternativas para comenzar a programar en Python, ya sea sin instalación mediante JupyterLab o con un entorno de desarrollo como PyCharm.

Cargado por

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

Guía Completa de Python para Principiantes

El documento es una guía completa sobre Python, que abarca desde la instalación hasta conceptos avanzados, dividida en 9 secciones que incluyen ejemplos prácticos y errores comunes. Se destaca la versatilidad de Python en diversas aplicaciones, como inteligencia artificial, desarrollo web y análisis de datos, y se menciona la importancia de su comunidad. Además, se ofrecen alternativas para comenzar a programar en Python, ya sea sin instalación mediante JupyterLab o con un entorno de desarrollo como PyCharm.

Cargado por

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

Contenido

El libro está dividido en 9 secciones. Empieza explicando las herramientas y después las pone en
práctica.

• 1 Presentación

• 2 Elige Tipo y Estructura de Datos

• 3 Control con Bucles y Condicionales

• 4 Reutiliza Código con Funciones

• 5 Programación Orientada a Objetos

• 6 Gestiona Errores con Excepciones

• 7 Testea tu Código

• 8 Top 50 Ejemplos Prácticos

• 9 Top 100 Errores Comunes

Puedes ver una muestra de los ejemplos aquí. Cubren de todo:

• � Inteligencia artificial, redes neuronales, genética y ADN.

• � Dashboards y representación gráfica.

• � Análisis financiero, apuestas, estadística y matemáticas.

• � Encriptado, firma digital, computación cuántica, factorización de números primos y fuerza


bruta.

• � Desarrollo de juegos, webs, APIs e interfaces de usuario.

• � Implementación de algoritmos sencillos.

FAQ

¿Necesito saber Python para leer este libro?

No, el libro está diseñado para que puedas leerlo sin saber Python.

¿Necesito saber programación para leer este libro?

No es necesario tener conocimientos avanzados de programación pero si una base mínima. Saber que
es una variable, un condicional o un bucle.

¿Hay libro físico?

Por ahora el libro solo está disponible en formato digital.

Soy una empresa o universidad, ¿cómo puedo comprar licencias?


Ofrecemos licencias corporativas para empresas y universidades. Cuando hagas la compra, podrás
seleccionar el número de licencias que necesites.

Introducción a Python

• 📗📗 ¿Qué es Python?

• 📗📗 Descargar e instalar Python

• 📗📗 Hola Mundo en Python

• 📗📗 Sintaxis Básica

• 📗📗 Nombrando variables I

• 📙📙 Palabras reservadas

• 📙📙 Alcance variables

• 📙📙 Ejecutando scripts

• 📙📙 Tipado dinámico y duck typing

• 📙📙 Funciones built-in

• 📙📙 Unpacking en Python

¿Qué es Python? Introducción

Si has llegado hasta aquí es posible que quieras aprender Python, o tal vez simplemente tengas
curiosidad por saber algo más de este lenguaje de programación tan popular. En este post te
intentaremos convencer de porqué debes aprender Python, sin importar en que trabajes o cual sea tu
sector.

Aprovechamos de paso a darte la bienvenida a nuestro libro, un lugar donde podrás encontrar tutoriales
de Python completamente gratis desde lo más básico a conceptos avanzados, y con diferentes
aplicaciones: análisis de datos, automatización de tareas, web scraping, machine learning, data
science, desarrollo de videojuegos, interfaces gráficos o finanzas.

Dicho esto, empecemos. A diferencia de lo que mucha gente puede pensar, Python es un lenguaje que
data de los años 1990s, y su creación se le atribuye al neerlandés Guido van Rossum[1]. Recibió su
nombre por los humoristas Monty Python.

Su última versión es Python 3, y es la que te recomendamos usar ya que las anteriores ya no tienen
soporte oficial. En este blog usaremos esta siempre para todo.

De acuerdo con StackOverflow insights[2] en la siguiente gráfica podemos ver el número de preguntas
vistas en la plataforma acerca de Pyhton. Podemos ver que Python lleva casi dos años en el podio, una
auténtica burrada.
De hecho un estudio realizado hace un tiempo en el 2017[3] ya ponía a Python como uno de los favoritos
en los países con mayores ingresos como Alemania, Reino Unido o Estados Unidos entre otros.

Python was the most visited tag on Stack Overflow within high-income nations

Podemos ver por lo tanto que existe una tendencia clara y seguramente el interés en Python vaya a seguir
creciendo en los próximos años. Estos son los motivos por lo que ha crecido tanto y lo seguirá haciendo:

• Se trata de un lenguaje fácil de aprender, con una sintaxis muy sencilla que se asemeja bastante
al pseudocódigo. En otras palabras, poco código hace mucho.

• Su uso no está ligado a un sector concreto. Por ejemplo el lenguaje R es útil para análisis de
datos, pero no puede ser usado para desarrollo web. Python vale para todo.

• Tiene una comunidad enorme, además de gran cantidad de librerías para hacer prácticamente
cualquier cosa, literalmente.

• Es un lenguaje multiplataforma, por lo que el mismo código es compatible en cualquier


plataforma (Windows, macOS, Linux) sin hacer nada.

• Por lo general se puede hacer desarrollos en Python más rápidamente que en otros lenguajes,
acortando la duración de los proyectos.

Usos de Python

Como hemos dicho Python es un lenguaje muy transversal, usado en diferentes industrias y para
diferentes fines. Veamos algunos de las empresas que usan Python:

• YouTube usa Python[4] en la parte del servidor, unida a otros lenguajes como Java o Go.
• Netflix usa Python[5] par automatizar tareas, explorar datos y labores de aprendizaje automático
entre otras.

• La NASA usa Python[6] en gran cantidad de programas científicos.

• JPMorgan[7] ya dijo hace varios años que se esperaba de sus analistas financieros supieran
Python.

Python es también usado para fines muy diversos como son los siguientes:

• Desarrollo Web: Existen frameworks como Django, Pyramid, Flask o Bottle que permiten
desarrollar páginas web a todos los niveles.

• Ciencia y Educación: Debido a su sintaxis tan sencilla, es una herramienta perfecta para enseñar
conceptos de programación a todos los niveles. En lo relativo a ciencia y cálculo numérico,
existen gran cantidad de librerías como SciPy o Pandas.

• Desarrollo de Interfaces Gráficos: Gran cantidad de los programas que utilizamos tienen un
interfaz gráfico que facilita su uso. Python también puede ser usado para desarrollar GUIs con
librerías como Kivy o pyqt.

• Desarrollo Software: También es usado como soporte para desarrolladores, como para testing.

• Machine Learning: En los último años ha crecido el número de implementaciones en Python de


librerías de aprendizaje automático como Keras, TensorFlow, PyTorch o sklearn.

• Visualización de Datos: Existen varias librerías muy usadas para mostrar datos en gráficas,
como matplotlib, seaborn o plotly.

• Finanzas y Trading: Gracias a librerías como QuantLib o qtpylib y a su facilidad de uso, es cada
vez más usado en estos sectores.

De hecho a día de hoy prácticamente cualquier API, librería o servicio que existe en el mundo tiene una
versión para Python, bien sea de manera nativa o a través de un wrapper.

Comunidad

La comunidad de Python es inmensa, con alrededor de 8.2 millones de personas en el mundo (a fecha
de 2019)[8], una cifra que supera ya a los usuarios de Java.

Es importante el tamaño de la comunidad, porque cuanto más grande sea, mayor soporte se le dará al
lenguaje y mayor número de personas compartirán sus problemas y se ayudarán a resolverlos.

Otra de las características de la comunidad Python son las famosas PyCon, unas convenciones anuales
llevadas a cabo en gran número de países, donde los desarrolladores se reúnen para compartir ideas.
Es también importante mencionar la Python Software Foundation, una organización sin ánimo de lucro
que se dedica a promover, proteger y desarrollar el lenguaje Python.

Características de Python

Como cualquier otro lenguaje, Python tiene una serie de características que lo hacen diferente al resto.
Las explicamos a continuación:

• Es un lenguaje interpretado, no compilado.

• Usa tipado dinámico, lo que significa que una variable puede tomar valores de distinto tipo.

• Es fuertemente tipado, lo que significa que el tipo no cambia de manera repentina. Para que se
produzca un cambio de tipo tiene que hacer una conversión explícita.

• Es multiplataforma, ya que un código escrito en macOS funciona en Windows o Linux y vice


versa.

Tal vez algunos de estos conceptos puedan resultarte extraños si estás empezando en el mundo de la
programación. El siguiente código pretende ilustrar algunas de las características de Python.

Algunas cosas curiosidad que en otros lenguajes no pasan. La función acepta un


parámetro entrada pero no se especifica su tipo. La x almacena primero una cadena, luego un float y
luego un integer. La función funcion() es llamada con un int, pero su valor se divide entre 2 y el resultado
es convertido automáticamente en un float.

def funcion(entrada):

return entrada/2

x = "Hola"

x = 7.0

x = int(x)
x = funcion(x)

print(x)

print(type(x))

# 3.5

# <class 'float'>

The Zen of Python

El Zen de Python es una colección de los 19 principios que influyen en el diseño del lenguaje. De alguna
manera, muestran la filosofía del mismo y pueden ser encontrados en la PEP20[9] Las PEP o Python
Enhancement Prososals son unos documentos que ofrecen información a la comunidad de Python, bien
describiendo alguna característica nueva o dando información en general.

Son las siguientes, y aunque alguna pueda parecer lógica, a veces resultan no serlo tanto cuando no se
cumplen:

1. Bello es mejor que feo.

2. Explícito es mejor que implícito.

3. Simple es mejor que complejo.

4. Complejo es mejor que complicado.

5. Plano es mejor que anidado.

6. Espaciado es mejor que denso.

7. La legibilidad es importante.

8. Los casos especiales no son lo suficientemente especiales como para romper las reglas.

9. Sin embargo la practicidad gana a la pureza.

10. Los errores nunca deben pasar silenciosamente.

11. A menos que se silencien explícitamente.

12. Frente a la ambigüedad, evitar la tentación de adivinar.

13. Debería haber una, y preferiblemente solo una, manera obvia de hacerlo.

14. A pesar de que esa manera no sea obvia a menos que seas Holandés (el creador lo era)

15. Ahora es mejor que nunca.

16. A pesar de que nunca es muchas veces mejor que ahora mismo.

17. Si la implementación es difícil de explicar, es una mala idea.


18. Si la implementación es fácil de explicar, puede que sea una buena idea.

19. Los namespaces son una gran idea, ¡tengamos más de esos!

¿Qué opinas? ¿Te hemos convencido para aprender Python? A continuación te explicaremos cómo
instalar Python para que puedas empezar a dar tus primeros pasos.

¿Cómo programar en Python?

Si quieres empezar a programar en Python, en este post te damos dos alternativas de como puedes
empezar a hacerlo:

• La primera es usar Python sin ningún tipo de instalación. Sin duda la más sencilla y rápida. Para
ello usaremos la versión online de JupyterLab.

• Y la segunda es usando Python con PyCharm, para lo que tendremos que instalar el propio
lenguaje Python y el entorno de desarrollo PyCharm.

¡Empecemos!

Sin instalación: JupyterLab

Si quieres empezar a programar en Python de manera rápida sin tener que instalar nada, existe una
alternativa muy útil llamada JupyterLab. Se trata de una herramienta desarrollada por Jupyter, un
proyecto open-source sin ánimo de lucro que nació en 2014 del proyecto IPython.

JupyterLab es un entorno de desarrollo web (se accede a el a través de Firefox, Chrome u otro navegador)
y además de poder instalártelo en tu ordenador, ofrecen un servicio online gratis de usar. Con tan sólo
entrar en una dirección web puedes empezar a programar.

Accede a https://jupyter.org/try y busca “Try JupyterLab”.


Una vez haya cargado la página encontrarás lo siguiente. Se trata del entorno de desarrollo que se nos
proporciona. A la izquierda tienes la navegación, donde están todos tus achivos y carpetas. A la derecha
se pueden visualizar los ficheros ipynb, que es el formato de los Jupyter Notebook por excelencia.

En estos ficheros ipynb puedes escribir código Python y ejecutarlo, además de poder mezclarlo con
texto, imágenes, animaciones y otras herramientas.

Si creamos un nuevo Notebook con File->New->Notebook y seleccionamos como Kernel Python3,


podemos empezar a crear nuestro primer código, el famoso “Hola Mundo”. Haciendo click en la flecha,
se puede ejecutar el código que tenemos seleccionado.

Los Notebook son una herramienta muy potente ya que:

• Permiten ejecutar código fragmento a fragmento, viendo el resultado justo en la siguiente línea.

• Se puede mezclar código con otros recursos como imágenes o texto con formato markdown,
entre otros recursos.

• Es posible visualizar también gráficas, como las generadas con matplotlib.

Pero también tiene sus desventajas:


• No se trata de un entorno de desarrollo completo, ya que carece de muchas funcionalidades.

• Si usamos la versión web, estaremos limitados por los recursos del servidor en el que se ejecute.
La rapidez será mayor en nuestro ordenador.

• Para proyectos grandes de Python no es una alternativa.

Por lo tanto es una herramienta perfecta para empezar, pero si crees que necesitas más, te explicamos
como instalar Python y el entorno de desarrollo PyCharm en la siguientes secciones.

Con instalación: Python + PyCharm

Si por lo contrario buscas algo más completo, deberás instalar Python y un entorno de desarrollo (IDE) en
tu ordenador. Necesitarás dos cosas:

• Por un lado necesitarás Python, es decir, el propio lenguaje de programación. Con esto y
cualquier editor de texto ya podrías programar, pero no es demasiado agradable.

• Por otro, es conveniente también instalar un entorno de desarrollo, ya que hace que programar
sea una tarea mucho más fácil. Existe muchos, como Atom, Sublime Text o Visual Studio Code,
pero nosotros usaremos PyCharm.

Instalar Python en Windows

Para instalar Python en Windows debes ir a la sección de descargas de la web oficial y seleccionar la
última versión. Te recomendamos utilizar la versión 3.x ya que aunque también existen versiones
anteriores como las 2.x, Python ya ha dejado de dar soporte a ellas.

Ambas versiones son relativamente similares, pero hay detalles o alguna que otra funcionalidad que
varía. En este blog nos centramos en la versión 3, por lo que todo el código que veas será compatible con
la misma.

Una vez hayas descargado el ejecutable, ábrelo y realiza la instalación. Es importante que verifiques que
se haya seleccionado la opción de “Add Python 3.x to PATH”

Una vez hayas finalizado, si abres el terminal de comandos de Windows (busca por la aplicación cmd o
símbolo de sistema) puedes verificar que se ha instalado correctamente ejecutando el siguiente
comando.

python -V

Y verás en la salida algo así dependiendo de la versión que hayas instalado.

Python 3.8.3
De hecho como hemos indicado, con esto ya podrías empezar a programar en Python en tu ordenador,
pero la verdad que no es demasiado cómodo. En el último apartado te explicaremos como instalar
PyCharm, un entorno de desarrollo que nos hará la vida mucho más fácil.

Instalar Python en macOS

Método 1

Existen dos formas diferentes de instalar Python en mac. Empezamos por la más sencilla. Accede a la
sección de descargas de la web oficial de Python y descarga la última versión. Te recomendamos usar la
versión 3, que es la que usamos en todo este blog y la más reciente.

Una vez lo hayas instalado y hayas terminado el proceso de instalación, puedes abrir el terminal y
verificar que efectivamente se ha instalado con el siguiente comando.

python -V

Y si el comando devuelve algo así como Python 3.6.8, ya estaría instalado. Si tecleas python en el
terminal ya podrías empezar a teclear comandos Python, pero no es muy cómodo. Más adelante te
explicaremos como usar Python con PyCharm.

Método 2

La segunda forma de instalar Python en mac es a través del gestor de paquetes homebrew. Primero
necesitarás instalar XCode con el siguiente comando.

xcode-select --install

Una vez instalado XCode deberás instalar Homebrew con el siguiente comando.

/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install.sh)"

Una vez instalado Homebrew ya podemos instalar Python a través de su gestor de paquetes.
Simplemente ejecuta.

brew install python3

Y por último verifica que Python se ha instalado correctamente.

python3 -V

Instalar Python en Linux


En la mayoría de distribuciones Linux se puede instalar de manera bastante fácil. Para el caso de Ubuntu
16.10 o más reciente, ejecuta el siguiente comando en el terminal. Dependiendo de la fecha, tal vez te
interese instalar una versión más reciente como la 3.8 u otra.

apt-get update

apt-get install python3.8

Si utilizas otra versión diferente de Ubuntu, deberás añadir un repositorio. Te


recomendamos deadsnakes PPA.

sudo apt-get install software-properties-common

sudo add-apt-repository ppa:deadsnakes/ppa

sudo apt-get update

sudo apt-get install python3.8

Y una vez finalizada la instalación puedes comprobar que se ha instalado correctamente con el siguiente
comando.

python -V

python3 -V

Instalando PyCharm

Llegados a este punto debemos ya tener instalado Python en nuestro ordenador, por lo que vamos a
proceder ya a instalar PyCharm.

Antes de nada, una breve introducción a PyCharm. Se trata de un entorno de desarrollo o IDE (Integrated
Development Environment) de la empresa JetBrains. Está disponible en las plataformas más comunes
como Windows, Linux o macOS y es una herramienta perfecta para escribir código en Python.

Para instalar PyCharm, accede a la sección de descargas y selecciona la versión Community, que es la
versión gratis de desarrollo. Una vez el proceso de instalación haya acabado, deberías ver algo así al
abrirlo.
A continuación explicaremos como crear y configurar un proyecto, para que puedas empezar a
programar en Python dentro del IDE PyCharm.

Configurando PyCharm

Una vez hayas abierto PyCharm, realiza los siguientes pasos:

• Crear un proyecto nuevo.

• Asigna un nombre y di donde lo quieres guardar.

• Te recomendamos usar Virtualenv para los entornos virtuales.

• En Base Interpreter selecciona la versión de Python que has descargado.

• El proyecto se habrá creado. Crea un nuevo fichero Python con un nombre.


• Ya puedes escribir código en el fichero. Empecemos por ejemplo con un Hola Mundo

• Y en Run->Run o click derecho y Run, podrás ejecutar el código.

Una de las características más útiles en PyCharm es que se pueden instalar paquetes de manera muy
sencilla a través de su interfaz gráfico. Pongamos que por ejemplo quisieras instalar la librería numpy. Ve
a Preferencias, Proyecto y en Proyect Interpreter podrás añadir librerías con haciendo click en el+.

Primeros pasos en Python

A continuación te ayudamos a dar tus primeros pasos en Python. ¡Allá vamos!

Hola mundo en Python

En cualquier introducción a un nuevo lenguaje de programación, no puede faltar el famoso Hola Mundo.
Se trata del primer programa por el que se empieza, que consiste en programar una aplicación que
muestra por pantalla ese texto. Si ejecutas el siguiente código, habrás cumplido el primer hito de la
programación en Python.

print("Hola Mundo")

Por lo tanto ya te puedes imaginar que la función print() sirve para imprimir valores por pantalla.
Imprimirá todo lo que haya dentro de los paréntesis. Fácil ¿verdad? A diferencia de otros lenguajes de
programación, en Python se puede hacer en 1 línea.

Definiendo variables en Python

Vamos a complicar un poco más las cosas. Creemos una variable que almacene un número. A diferencia
de otros lenguajes de programación, no es necesario decirle a Python el tipo de dato que queremos
almacenar en x. En otros lenguajes es necesario especificar que x almacenará un valor entero, pero no
es el caso. Python es muy listo y al ver el número 5, sabrá de que tipo tiene que ser la x.

x=5

Ahora podemos juntar el print() que hemos visto con la x que hemos definido, para en vez de imprimir
el Hola Mundo, imprimir el valor de la x.

print(x)

# Salida: 5

En el anterior fragmento habrás visto el uso #. Se trata de la forma que tiene Python de crear los
denominados comentarios. Un comentario es un texto que acompaña al código, pero que no es código
propiamente dicho. Se usa para realizar anotaciones sobre el código, que puedan resultar útiles a otras
personas. En nuestro caso, simplemente lo hemos usado para decirte que la salida de ese comando
será 5, ya que x valía 5.

Sumando variables en Python

Vamos a sumar dos variables e imprimir su valor. Lo primero vamos a declararlas, con nombres a y b.
Declarar una variable significa “crearla”.

# Declaramos las variables a, b

# y asignamos dos valores

a=3

b=7

Ahora Python ya conoce a y b y sus respectivos valores. Podemos hacer uso de + para sumarlos, y una
vez más de print() para mostrar su valor por pantalla.

print(a+b)

Es importante que sólo usemos variables que hayan sido definidas, porque de lo contrario tendremos un
error. Si hacemos:

print(z)

# NameError: name 'z' is not defined

Tendremos un error porque Python no sabe que es z, ya que no ha sido declarada con anterioridad.

Ejemplo condicional

Podemos empezar a complicar un poco más las cosas con el uso de una sentencia condicional. Te lo
explicamos más adelante en este post sobre el if.

El siguiente código hace uso del if para comprobar si la a es igual == a 10. Si lo es, se imprimirá “Es 10” y
si no lo es “No es 10”. Es importante el uso de ==, que es el operador relacional que veremos en otros
posts.
a = 10

if a == 10:

print("Es 10")

else:

print("No es 10")

Decimales y cadenas

De la misma forma que hemos visto que una variable puede almacenar un valor entero como 10, es
posible también almacenar otros tipos como decimales o incluso cadenas de texto.

Si queremos almacenar un valor decimal, basta con indicarlo usando la separación con .

valor_decimal = 10.3234

Y si queremos almacenar una cadena, es necesario indicar su contenido entre comillas simples 'o
dobles ".

mi_cadena = "Hola Mundo"

Esperamos que te haya resultado útil esta introducción, y con ella ya estas list@ para continuar al
siguiente tutorial, donde veremos más acerca de la sintaxis de Python.

Sintaxis Python

A continuación veremos la sintaxis de Python, viendo como podemos empezar a usar el lenguaje
creando nuestras primeras variables y estructuras de control.

El termino sintaxis hace referencia al conjunto de reglas que definen como se tiene que escribir el
código en un determinado lenguaje de programación. Es decir, hace referencia a la forma en la que
debemos escribir las instrucciones para que el ordenador, o más bien lenguaje de programación, nos
entienda.

En la mayoría de lenguajes existe una sintaxis común, como por ejemplo el uso de = para asignar un dato
a una variable, o el uso de {} para designar bloques de código, pero Python tiene ciertas particularidades.

La sintaxis es a la programación lo que la gramática es a los idiomas. De la misma forma que la frase
“Yo estamos aquí” no es correcta, el siguiente código en Python no sería correcto, ya que no respeta las
normas del lenguaje.

if ($variable){

x=9;

Lo veremos a continuación en detalle, pero Python no soporta el uso de $ ni hace falta terminar las líneas
con ; como en otros lenguajes, y tampoco hay que usar {} en estructuras de control como en el if.
Por otro lado, de la misma forma que un idioma no se habla con simplemente saber todas sus palabras,
en la programación no basta con saber la sintaxis de un lenguaje para programar correctamente en él. Es
cierto que sabiendo la sintaxis podremos empezar a programar y a hacer lo que queramos, pero el uso
de un lenguaje de programación va mucho más allá de la sintaxis.

Para empezar a perderle el miedo a la sintaxis de Python, vamos a ver un ejemplo donde
vemos cadenas, operadores aritméticos y el uso del condicional if.

El siguiente código simplemente define tres valores a, b y c, realiza unas operaciones con ellos y muestra
el resultado por pantalla.

# Definimos una variable x con una cadena

x = "El valor de (a+b)*c es"

# Podemos realizar múltiples asignaciones

a, b, c = 4, 3, 2

# Realizamos unas operaciones con a,b,c

d = (a + b) * c

# Definimos una variable booleana

imprimir = True

# Si imprimir, print()

if imprimir:

print(x, d)

# Salida: El valor de (a+b)*c es 14

Como puedes observar, la sintaxis de Python es muy parecida al lenguaje natural o pseudocódigo, lo que
hace que sea relativamente fácil de leer. Otra ventaja es que no necesitamos nada más, el código
anterior puede ser ejecutado tal cual está. Si conoces otros lenguajes como C o Java, esto te resultará
cómodo, ya que no es necesario crear la típica función main().

Comentarios

Los comentarios son bloques de texto usados para comentar el código. Es decir, para ofrecer a otros
programadores o a nuestro yo futuro información relevante acerca del código que está escrito. A efectos
prácticos, para Python es como si no existieran, ya que no son código propiamente dicho, solo
anotaciones.

Los comentarios se inician con # y todo lo que vaya después en la misma línea será considerado un
comentario.

# Esto es un comentario

Al igual que en otros lenguajes de programación, podemos también comentar varias líneas de código.
Para ello es necesario hacer uso de triples comillas bien sean simples ''' o dobles """. Es necesario
usarlas para abrir el bloque del comentario y para cerrarlo.

'''

Esto es un comentario

de varias líneas

de código

'''

Indentación y bloques de código

En Python los bloques de código se representan con indentación, y aunque hay un poco de debate con
respecto a usar tabulador o espacios, la norma general es usar cuatro espacios.

En el siguiente código tenemos un condicional if. Justo después tenemos un print() indentado con cuatro
espacios. Por lo tanto, todo lo que tenga esa indentación pertenecerá al bloque del if.

if True:

print("True")

Esto es muy importante ya que el código anterior y el siguiente no son lo mismo. De hecho el siguiente
código daría un error ya que el if no contiene ningún bloque de código, y eso es algo que no se puede
hacer en Python.

if True:

print("True")

Por otro lado, a diferencia de en otros lenguajes de programación, no es necesario utilizar ; para terminar
cada línea.

# Otros lenguajes como C

# requieren de ; al final de cada línea

x = 10;

Sin embargo en Python no es necesario, basta con un salto de línea.

x=5
y = 10

Pero se puede usar el punto y coma ; para tener dos sentencias en la misma línea.

x = 5; y = 10

Múltiples líneas

En algunas situaciones se puede dar el caso de que queramos tener una sola instrucción en varias línea
de código. Uno de los motivos principales podría ser que fuera demasiado larga, y de hecho en la
especificación PEP8 se recomienda que las líneas no excedan los 79 caracteres.

Haciendo uso de \ se puede romper el código en varias líneas, lo que en determinados casos hace que
el código sea mucho más legible.

x = 1 + 2 + 3 + 4 +\

5+6+7+8

Si por lo contrario estamos dentro de un bloque rodeado con paréntesis (), bastaría con saltar a la
siguiente línea.

x = (1 + 2 + 3 + 4 +

5 + 6 + 7 + 8)

Se puede hacer lo mismo para llamadas a funciones

def funcion(a, b, c):

return a+b+c

d = funcion(10,

23,

3)

Creando variables

Anteriormente ya hemos visto como crear una variable y asignarle un valor con el uso de =. Existen
también otras formas de hacerlo de una manera un poco más sofisticada.

Podemos por ejemplo asignar el mismo valor a diferentes variables con el siguiente código.

x = y = z = 10

O también podemos asignar varios valores separados por coma.

x, y = 4, 2

x, y, z = 1, 2, 3
Nombrando variables

Puedes nombrar a tus variables como quieras, pero es importante saber que las mayúsculas y
minúsculas son importantes. Las variables x y X son distintas.

Por otro lado existen ciertas normas a la hora de nombrar variables:

• El nombre no puede empezar por un número

• No se permite el uso de guiones -

• Tampoco se permite el uso de espacios.

Se muestran unos ejemplos de nombres de variables válidos y no válidos.

# Válido

_variable = 10

vari_able = 20

variable10 = 30

variable = 60

variaBle = 10

# No válido

2variable = 10

var-iable = 10

var iable = 10

Una última condición para nombrar a una variable en Python, es no usar nombres reservados para
Python. Las palabras reservadas son utilizadas por Python internamente, por lo que no podemos usarlas
para nuestras variables o funciones.

import keyword

print(keyword.kwlist)

# ['False', 'None', 'True', 'and', 'as', 'assert',

# 'async', 'await', 'break', 'class', 'continue',

# 'def', 'del', 'elif', 'else', 'except', 'finally',

# 'for', 'from', 'global', 'if', 'import', 'in', 'is',

# 'lambda', 'nonlocal', 'not', 'or', 'pass', 'raise',


# 'return', 'try', 'while', 'with', 'yield']

De hecho con el siguiente comando puedes ver todas las palabras clave que no puedes usar.

import keyword

print(keyword.kwlist)

Uso de paréntesis

Python soporta todos los operadores matemáticos más comunes, conocidos como operadores
aritméticos. Por lo tanto podemos realizar sumas, restas, multiplicaciones, exponentes (usando **) y
otros que no vamos a explicar por ahora. En el siguiente ejemplo realizamos varias operaciones en la
misma línea, y almacenamos su resultado en y.

x = 10

y = x*3-3**10-2+3

Pero el comportamiento del código anterior y el siguiente es distinto, ya que el uso de paréntesis () da
prioridad a unas operaciones sobre otras.

x = 10

y = (x*3-3)**(10-2)+3

El uso de paréntesis no solo se aplica a los operadores aritméticos, sino que también pueden ser
aplicados a otros operadores como los relacionales o de membresía que vemos en otros posts.

Variables y alcance

Un concepto muy importante cuando definimos una variable, es saber el alcance o scope que tiene. En
el siguiente ejemplo la variable con valor 10 tiene un alcance global y la que tiene el valor 5 dentro de la
función, tiene un alcance local. Esto significa que cuando hacemos print(x), estamos accediendo a la
variable global x y no a la x definida dentro de la función.

x = 10

def funcion():

x=5

funcion()

print(x)

No te preocupes si no lo has entendido. Es un concepto un poco complicado de pillar al principio, pero


lo veremos más adelante. Te recomendamos leer los siguientes posts para entender mejor las funciones
y el alcance de las variables:
• Funciones en Python y sus argumentos

• Paso por valor y por referencia

• Variable global en Python

Uso de la función print()

Por último, en cualquier lenguaje de programación es importante saber lo que va pasando a medida que
se ejecutan las diferentes instrucciones. Por ello, es interesante hacer uso de print() en diferentes
secciones del código, ya que nos permiten ver el valor de las variables y diferente información útil.

Existen muchas formas de usar la función print() y te las explicamos en detalle en este post, pero por
ahora basta con que sepas lo básico.

Como ya hemos visto se puede usar print() para imprimir por pantalla el texto que queramos.

print("Esto es el contenido a imprimir")

También es posible imprimir el contenido de una variable.

x = 10

print(x)

Y separando por comas , los valores, es posible imprimir el texto y el contenido de variables.

x = 10

y = 20

print("Los valores x, y son:", x, y)

# Salida: Los valores x, y son: 10 20

Nombrando variables

Crear variables

Las variables en Python se pueden crear asignando un valor a un nombre sin necesidad de declararla
antes.

x = 10

y = "Nombre"

z = 3.9

Nombres de variables

Podemos asignar el nombre que queramos, respetando no usar las palabras reservadas de Python ni
espacios, guiones o números al principio.

# Válido
_variable = 10

vari_able = 20

variable10 = 30

variable = 60

variaBle = 10

Los siguientes ejemplos no son permitidos.

# No válido

2variable = 10

var-iable = 10

var iable = 10

Asignar múltiples valores

Se pueden asignar múltiples variables en la misma línea.

x, y, z = 10, 20, 30

Imprimir variables

Una variable puede ser impresa por pantalla usando print()

x = 10

y = "Nombre"

print(x)

print(y)

Palabras reservadas en Python

Python tiene un conjunto de palabras reservadas que no podemos utilizar para nombrar variables ni
funciones, ya que las reserva internamente para su funcionamiento.

Por ejemplo, no podemos llamar a una función True, y si intentamos hacerlo, tendremos un SyntaxError.
Esto es lógico ya que Python usa internamente True para representar el tipo booleano.

def True():

pass

# SyntaxError: invalid syntax

Análogamente, no podemos llamar a una variable is ya que se trata del operador de identidad.
is = 4

# SyntaxError: invalid syntax

Resulta lógico que no se nos permita realizar esto, ya que de ser posible, podríamos romper el lenguaje.
Algo muy importante a tener en cuenta es que palabras como list no están reservadas, y esto es algo que
puede generar problemas. El siguiente código crea una lista usando la función estándar de Python list().

a = list("letras")

print(a)

# ['l', 'e', 't', 'r', 'a', 's']

Sin embargo, y aunque pueda parece extraño, podemos crear una función con ese nombre. Al hacer
esto, nos estamos cargando la función list() de Python, y por lo tanto al intentar hacer la llamada anterior
falla, ya que nuestra función en este caso no acepta argumentos. Mucho cuidado con esto.

def list():

print("Funcion list")

a = list("letras")

# TypeError: list() takes 0 positional arguments but 1 was given

Pero volviendo a las palabras reservadas, Python nos ofrece una forma de acceder a estas
palabras programmatically, es decir, a través de código. Aquí tenemos un listado con todas las palabras
reservadas.

import keyword

print(keyword.kwlist)

# ['False', 'None', 'True', 'and', 'as', 'assert',

# 'async', 'await', 'break', 'class', 'continue',

# 'def', 'del', 'elif', 'else', 'except', 'finally',

# 'for', 'from', 'global', 'if', 'import', 'in', 'is',

# 'lambda', 'nonlocal', 'not', 'or', 'pass', 'raise',

# 'return', 'try', 'while', 'with', 'yield']

Vistas ya las palabras reservadas de Python, a continuación explicaremos para que sirve cada una de
ellas y las pondremos en contexto.

Condicionales: if, elif, else


El uso del if y los condicionales en general son la base de cualquier lenguaje de programación. Son
usados para alterar la línea de ejecución del programa en función de determinadas condiciones.

En el siguiente ejemplo podemos ver su uso. De los tres bloques, sólo se ejecutará uno de ellos, el cual
cumpla la condición establecida sobre lenguaje.

lenguaje = "Python"

if lenguaje == "Python":

print("Estoy de acuerdo, Python es el mejor")

elif lenguaje == "Java":

print("No me gusta, Java no mola")

else:

print("Ningún otro lenguaje supera a Python")

# Salida: Estoy de acuerdo, Python es el mejor

Bucles: while, for, break, continue

El while y for permiten crear bucles que ejecutan una sección de código repetidas veces. Por otro lado
el continue y el break permiten realizar alteraciones sobre el bucle.

El while ejecuta su sección de código mientras se cumpla una determinada condición.

x=0

while x < 3:

print(x)

x += 1

# Salida: 0, 1, 2

El for permite iterar clases iterables, ejecutando la sección de código tantas veces como elementos
tiene el iterable.

for i in range(3):

print(i)

# Salida: 0, 1, 2
El continue salta hasta el final del bloque, dejando sin ejecutar lo restante, pero continúa en la
siguiente iteración.

for i in range(3):

if i == 1:

continue

print(i)

# Salida: 0, 2

Por último, el break rompe la ejecución del bucle, saliendo del mismo.

x=0

while True:

print(x)

if x == 2:

break

x += 1

# Salida: 0, 1, 2

Valores: False, True, None

False, True y None son valores que pueden ser asignados a variables, siendo los dos
primeros booleanos y el último algo parecido al null de otros lenguajes de programación.

Si realizamos una comparación usando el operador relacional == se nos devolverá True o False.

x = (5 == 1)

print(x)

# Salida: False

También podemos asignar nosotros True a una variable.

x = True

if x:

print("Python!")
# Salida: Python!

Por otro lado None se devuelve por defecto cuando una función no cuenta con un return.

def mi_funcion():

pass

print(mi_funcion())

# Salida: None

Operadores lógicos: and, or, not

El and, or y not son operadores lógicos que actúan sobre valores booleanos. El primero es verdadero si y
solo si todos los operandos son verdaderos. El segundo devuelve verdadero si al menos un elemento es
verdadero. Y por último, el not invierte verdadero por falso y viceversa.

print(True and False) # False

print(True or False) # True

print(not True) # False

Funciones: def, return, lambda, pass, yield

Todas ellas relacionadas con las funciones. El uso de def nos permite crear una función.

def funcion_suma(a, b):

print("La suma es", a + b)

funcion_suma(3, 5)

# Salida: La suma es 8

Si queremos que la función devuelva uno o varios valores, podemos usar return.

def funcion_suma(a, b):

return a + b

suma = funcion_suma(3, 5)

print("La suma es", suma)


# Salida: La suma es 8

El uso de lambda nos permite crear funciones lambda, una especie de funciones “para vagos”. Dichas
funciones no tienen un nombre per se, salvo asignado explícitamente.

print("La suma es", (lambda a, b: a + b)(3, 5))

# Salida: La suma es 8

Por otro lado, podemos usar pass cuando no queramos definir la función, es decir si la queremos dejar
en blanco por el momento. Nótese que también puede ser usado en clases, estructuras de control, etc.

def funcion_suma(a, b):

pass

Por último, yield está asociado a los generadores y las corrutinas, un concepto un tanto avanzado pero
muy interesante. En el siguiente generador vemos como se generan tres valores, obteniendo uno cada
vez que iteramos el generador.

def generador():

n=1

yield n

n += 1

yield n

n += 1

yield n

for i in generador():

print(i)

# Salida: 1, 2, 3

Los generadores pueden ser usados para generar secuencias infinitas de valores, sin que tengan que ser
almacenados a priori, siendo creados bajo demanda. Este es una utilidad muy importante trabajando
con listas muy grandes, cuyo almacenamiento en memoria sería muy costoso.

Clases: class
El uso de class nos permite crear clases. Las clases son el núcleo de la programación orientada objetos,
y son una especie de estructura de datos que agrupa un conjunto de funciones (métodos) y variables
(atributos).

class MiClase:

def __init__(self):

print("Creando objeto de MiClase")

objeto = MiClase()

# Salida: Creando objeto de MiClase

Excepciones: assert, try, except, finally, raise

Las palabras clave assert, try, except, finally y raise están relacionadas con las excepciones, y nos
permiten tratar el qué hacer cuando las cosas no salen como esperamos. El siguiente código intenta
hacer un cast de cadena a entero, manejando un posible error.

• Si x="10" el casteo se realiza sin problemas, ya que es posible representar esa cadena como un
entero. Sin embargo hay que estar preparados siempre para lo peor.

• Si x="a" no se podría hacer int() y tendríamos un error. Si no manejamos este error, el programa se
pararía, y esto no es algo deseable. El uso de try, except y finally nos permite controlar dicho error
y actuar en consecuencia sin que el programa se pare.

x = "10"

valor = None

try:

valor = int(x)

except Exception as e:

print("Hubo un error:", e)

finally:

print("El valor es", valor)

# Salida: El valor es 10

Variables: global, nonlocal


El uso de global permite realizar lo siguiente, y de no usarlo tendríamos un UnboundLocalError. Aunque
puede resultar muy útil, mucho cuidado con las variables globales.

a=0

def suma_uno():

global a

a=a+1

suma_uno()

print(a)

# Salida: 1

El uso de nonlocal es útil cuando tenemos funciones anidadas. En el siguiente ejemplo podemos ver
como cuando funcion_b modifica x, también afecta a la x de la funcion_a, ya que la hemos declarado
como nonlocal. Te invitamos a que elimines el nonlocal y veas el comportamiento.

def funcion_a():

x = 10

def funcion_b():

nonlocal x

x = 20

print("funcion_b", x)

funcion_b()

print("funcion_a", x)

funcion_a()

# Salida:
# funcion_b 20

# funcion_a 20

Al usar nonlocal, el valor de x se modifica en el ámbito de funcion_a desde funcion_b.

Sin nonlocal, Python trataría x dentro de funcion_b como una variable local a esa función, y el valor
de x en funcion_a no se modificaría.

Módulos: from, import

El uso de from e import nos permite importar módulos o librerías, tanto estándar de Python como
externas o definidas por nosotros. En ejemplos como este es donde podemos ver que la sintaxis de
Python se asemeja bastante al lenguaje natural: de collections importa namedtuple.

from collections import namedtuple

Pertenencia e Identidad: in, is

El uso de in nos permite saber si un determinado elemento está en una clase iterable,
devolviendo True en el caso de que sea cierto.

lista = ["a", "b", "c"]

print("a" in lista)

# Salida: True

El uso de is nos permite saber si dos variables apuntan en realidad al mismo objeto. Por debajo se usa la
función id() y es importante notar que la igualdad == no implica que is sea True.

a = [1, 2]

b = [1, 2]

c=a

print(a is b) # False

print(a is c) # True

Eliminar variables: del

El uso de del nos permite eliminar una variable del scope, pudiendo resultar útil cuando trabajamos con
variables que almacenan gran cantidad de datos. Es una manera explícita de indicar que ya no queremos
una variable, pero no olvidemos que Python tiene garbage collector.

a = 10

del a
print(a)

# Salida: NameError: name 'a' is not defined

Context Managers: with, as

El uso de with y as es muy utilizado a la hora de manejar ficheros, pero en realidad pertenecen a
los context managers o gestores de contexto, un concepto algo avanzado.

with open('fichero.txt', 'r') as file:

print(file.read())

Concurrencia: async, await

El uso de async y await nos permite ejecutar procesos de manera concurrente en vez de secuencial.
Imaginemos un proceso() que tarda 10 segundos en ejecutarse, ya que realiza una petición a una base
de datos que lo bloquea durante ese tiempo. Sin esta herramienta, si quisiéramos ejecutar 3 veces el
proceso tardaríamos 30 segundos, ya que por defecto se ejecutan de manera secuencial, hasta que uno
no acaba no pasamos al siguiente.

Sin embargo, creando una función async y usando await, podemos paralelizar la ejecución de los
procesos, aprovechando el tiempo “muerto” mientras se retorna al await. En el siguiente ejemplo
podemos ver como se tarda unos 10 segundos en ejecutar los 3 procesos.

import asyncio

async def proceso(id_proceso):

print("Empieza proceso:", id_proceso)

await asyncio.sleep(10)

print("Acaba proceso:", id_proceso)

async def main():

await asyncio.gather(proceso(1), proceso(2), proceso(3))

asyncio.run(main())

# Salida:

# Empieza proceso: 1
# Empieza proceso: 2

# Empieza proceso: 3

# Acaba proceso: 1

# Acaba proceso: 2

# Acaba proceso: 3

Alcance variables en Python

Python clasifica los alcances de acuerdo con el modelo LEGB, que determina el orden en que se buscan
las variables. Las siglas LEGB corresponden a:

Local: Variables definidas dentro de una función o bloque, accesibles únicamente dentro de ese
contexto. Se crean al inicio de la ejecución de la función y desaparecen cuando esta termina.

def saludo():

mensaje = "¡Hola, mundo!" # Variable local

print(mensaje)

saludo()

print(mensaje) # Esto generará un error porque 'mensaje' es local a la función.

En este caso, la variable mensaje se define y utiliza dentro de la función saludo. Fuera de esta función, la
variable no existe y no se puede acceder.

Enclosing: Se refiere a las variables definidas en una función exterior que encapsula otra función. Estas
variables no son locales para la función más interna, pero tampoco son globales.

def funcion_exterior():

mensaje = "Hola desde la función exterior" # Variable en el alcance 'enclosing'

def funcion_interior():

print(mensaje) # La función interior accede a la variable de la exterior

funcion_interior()

funcion_exterior()

Salida: Hola desde la función exterior


En este caso, mensaje no es ni local para funcion_interior ni global, sino que pertenece al alcance
de funcion_exterior. Este es un ejemplo de una variable con alcance enclosing.

Global: Variables definidas en el nivel superior del script(/ejecutar-script-python) o programa, fuera de


cualquier función. Son accesibles en todo el archivo, pero pueden ser modificadas dentro de una
función solo si se usa la palabra clave global.

mensaje = "Hola desde el alcance global" # Variable global

def imprimir_mensaje():

print(mensaje) # Accediendo a la variable global

imprimir_mensaje()

print(mensaje) # También accesible fuera de las funciones

Salida: Hola desde el alcance global Hola desde el alcance global

En este caso, mensaje se define fuera de cualquier función(/funciones-python), por lo que es global y
puede ser utilizada tanto dentro como fuera de las funciones(/funciones-python).

Built-in: Variables y funciones predefinidas de Python que están disponibles en cualquier parte del
código, como len(), range() y print().

from math import sqrt # 'sqrt' es una función built-in del módulo 'math'

numero = 16

raiz_cuadrada = sqrt(numero) # Usamos la función built-in para calcular la raíz cuadrada

print(f"La raíz cuadrada de {numero} es {raiz_cuadrada}")

Salida: La raíz cuadrada de 16 es 4.0

En este caso, utilizamos una función(/funciones-python) built-in del módulo estándar math, que amplía
las funcionalidades predefinidas de Python.

Trabaja con ficheros con os

En este ejemplo vemos como trabajar con ficheros usando os. Vemos como:

• � Crear ficheros

• � Modificar el nombre de ficheros


• � Eliminar ficheros y carpetas

� Veamos como crear varios ficheros. Los creamos vacíos, pero puedes usar cambiar el write para
añadir contenido si lo deseas.

def crea_ficheros(ruta, nombre, cantidad):

if not os.path.exists(ruta):

os.makedirs(ruta)

for i in range(cantidad):

archivo_ruta = os.path.join(ruta, f"{nombre}_{i}.txt")

with open(archivo_ruta, 'w') as fichero:

fichero.write('')

print(f"Creado: {archivo_ruta}")

Si llamamos a la función, crearemos 10 ficheros en la carpeta ejemplo con nombres


desde fichero_1.txt hasta fichero_9.txt.

crea_ficheros("./ejemplo", "fichero", 10)

� Veamos como modificar el nombre de los ficheros. Esta función añade un prefijo al nombre de cada
fichero en la ruta. Es decir:

• Renombra fichero.txt a prefijo_fichero.txt.

def renombra_ficheros(ruta, prefijo):

for contenido in os.listdir(ruta):

archivo_ruta = os.path.join(ruta, contenido)

if os.path.isfile(archivo_ruta):

nuevo_nombre = prefijo + contenido

nueva_ruta = os.path.join(ruta, nuevo_nombre)

os.rename(archivo_ruta, nueva_ruta)

print(f"{contenido} -> {nuevo_nombre}")

Podemos usar la función así.

renombra_ficheros("./ejemplo", "prefijo_")
� Veamos como eliminar ficheros y carpetas. Esta función elimina todos los ficheros de ruta.

def elimina_ficheros(ruta):

for root, dirs, files in os.walk(ruta, topdown=False):

for file in files:

os.remove(os.path.join(root, file))

print(f"Eliminado: {os.path.join(root, file)}")

os.rmdir(ruta)

print(f"Archivos y directorio eliminados: {ruta}")

Y la puedes llamar así.

elimina_ficheros("./ejemplo")

� Ejercicios:

• En vez de añadir prefijo_ a todos los ficheros, modifica la función para añadir un número que
indique su orden alfabético. Por ejemplo para ejemplo.txt y archivo.txt el prefijo a añadir
sería 1_archivo.txt y 2_ejemplo.txt. La a va antes que la e alfabéticamente.

Bases de datos con sqlite

Veamos como trabajar con bases de datos para persistir nuestra información en el disco y que pueda ser
usada una vez nuestro código ha terminado. Sin algún tipo de persistencia, toda la información que tu
programa sabe es olvidada una vez este acaba.

Hay muchas formas de hacerlo. Puedes almacenar la información en un simple fichero. O puedes usar
bases de datos relacionales y lenguajes como SQL.

En este ejemplo usaremos sqlite3 para almacenar nuestros gastos mensuales en una base de
datos gastos.db. Se trata de un paquete muy sencillo de utilizar y a diferencia de otros no requiere un
servidor externo.

Empezamos creando la base de datos y una tabla llamada gastos con dos campos:

• � categoria: Tipo de gasto almacenado. Es un texto TEXT.

• � cantidad: Cantidad del gasto. Es un número real REAL.

Como puedes ver exigimos que en ambos casos el campo no puede ser NULL. Es decir, que no lo puedes
dejar vacío. Todo el contenido de nuestra base de datos se almacenará en el fichero gastos.db.

import sqlite3

def conecta_db():
return sqlite3.connect('gastos.db')

def crea_tabla():

with conecta_db() as conn:

conn.execute('''

CREATE TABLE IF NOT EXISTS gastos (

categoria TEXT NOT NULL,

cantidad REAL NOT NULL

)''')

Ahora necesitamos una función para añadir gastos a nuestra tabla.

def add_gasto(categoria, cantidad):

with conecta_db() as conn:

conn.execute('''

INSERT INTO gastos (categoria, cantidad)

VALUES (?, ?)''', (categoria, cantidad))

También una función para obtener los gastos. Esta función consulta la base de datos y nos devuelve los
gastos que han sido almacenados. También puedes usar categoria para filtrar los gastos por categoría. Si
no lo usas, se devolverán todos los gastos.

def get_gastos(categoria=None):

with conecta_db() as conn:

cursor = conn.cursor()

query = 'SELECT * FROM gastos WHERE '

condiciones, parametros = [], []

if categoria:

condiciones.append("categoria = ?")

parametros.append(categoria)

query += " AND ".join(condiciones) if condiciones else "1=1"


cursor.execute(query, parametros)

return cursor.fetchall()

Ahora vamos a crear nuestros gastos. Creamos la tabla y usamos la función add_gasto para añadir
nuestros gastos.

crea_tabla()

add_gasto("Transporte", 5)

add_gasto("Comida", 5)

add_gasto("Comida", 7)

add_gasto("Alquiler", 300)

Comprobamos que han sido almacenados correctamente. Deberíamos obtener los mismos que hemos
introducido.

gastos = get_gastos()

for gasto in gastos:

print(gasto)

Consultamos todos los gastos de una categoría concreta.

gastos = get_gastos(categoria="Comida")

for gasto in gastos:

print(gasto)

Como puedes ver tus gastos se persisten en el disco duro aunque tu código haya terminado.

� Ejercicios:

• Añade a la tabla gastos de tu base de datos un campo llamado fecha, que almacene cuando se
realizó el gasto.

• Añade a get_gastos la posibilidad de filtrar por fecha. Por ejemplo, los gastos de un mes concreto.

• Añade todas las operaciones CRUD sobre la base de datos. Las operaciones CRUD son Create,
Remove, Update y Delete. Tenemos add_gasto y get_gastos. Añade remove_gasto y update_gasto.

Patrones con re

Veamos cómo validar si una dirección de correo está bien escrita. Aunque es algo que el ser humano ve
en un golpe de vista, validar esto en programación es un poco más complicado.

• � esto@no.es@un.correo@valido Dirección de correo no válida.


• � nombre@dominio.es Dirección de correo válida.

Para resolver este problema usaremos las expresiones regulares o regular expressions. Estas nos
permiten buscar patrones en texto. Este ejemplo nos permite detectar números en una cadena. Como
puedes ver se detectó el 2 y el 3.

import re

r1 = re.findall(r"\d+", "Tengo 2 gatos y 3 perros")

print(r1) # ['2', '3']

También podemos reemplazar texto. En este caso reemplazamos 2 y 3 por dos y tres respectivamente.

r2 = re.sub(r"2", "dos", "Tengo 2 gatos y 3 perros")

r2 = re.sub(r"3", "tres", r2)

print(r2)

# Tengo dos gatos y tres perros

Pero también validar que un texto cumple con unas normas. Por ejemplo, sabemos que un correo
electrónico como usuario@gmail.comestá compuesto por:

• � Un nombre de usuario.

• � Seguido de @.

• � Un dominio como gmail.

• � Un TLD como .com.

Podemos por tanto escribir una expresión que nos diga si una dirección de correo es válida o no de la
siguiente manera.

def valida_email(email):

usuario = r"[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+"

arroba = r"@"

dominio = r"[a-zA-Z0-9-]+"

tld = r"(?:\.[a-zA-Z0-9-]+)*"

email_regex = rf"^{usuario}{arroba}{dominio}{tld}$"

return re.match(email_regex, email) is not None


print(valida_email("juan@gmail.com")) # True

print(valida_email("juan@gmail.com.ar")) # True

print(valida_email("653fsaasd")) # False

print(valida_email("juan@gma@.dom.com")) # False

print(valida_email("@@@")) # False

Puede parecer complicado, así que veamos parte por parte:

• � usuario: Para el nombre de usuario se permiten números, minúsculas, mayúsculas y algunos


caracteres raros. Por ejemplo [a-zA-Z0-9]+ indica que se permiten números, minúsculas y
mayúsculas. El + indica que buscamos uno o muchos caracteres de este tipo. El resto
como ! o $ indica que estos caracteres también son permitidos.

• � arroba: Buscamos que la @ esté presente en el lugar adecuado.

• � dominio: Requerimos un dominio con números, minúsculas, mayúsculas y guiones.

• � tld: Requiere que empiece con un . y permitimos múltiples. Es decir, .com y co.uk son válidos.

� Ejercicios:

• Modifica la función para que solo permita dominios .com.

• Modifica la función para que no admita dominios @google.

Analítica de futbol con seaborn

En este ejemplo vamos a representar un mapa de calor o heatmap desde donde se iniciaron más pases
en el partido de fútbol España-Inglaterra del 14 de julio de 2024.

• � Con statsbombpy obtenemos los datos del partido

• � Con seaborn representamos la información gráficamente.

Primero obtenemos los datos usando el match_id de ese partido. Después filtramos los datos para
obtener el origen de los pases que realizó Inglaterra a lo largo del partido.

from statsbombpy import sb

import seaborn as sns

import matplotlib.pyplot as plt

ev = sb.events(match_id=3943043)

ev = ev.loc[

(ev['type'] == "Pass") &


(ev['team'] == "England")]

Llegados aquí, en ev tenemos todos los eventos relativos a pases de Inglaterra. Pero lo más importante
para el ejemplo es:

• � El campo location: El punto de inicio de cada pase.

• � El campo pass_end_location: El punto donde finalizó el pase.

Ahora con seaborn representamos el heatmap de donde se iniciaron los pases. Esto nos permite
hacernos una idea de los puntos calientes desde donde se inician los pases. Podemos ver que el portero
inició muchos saques.

plt.figure(figsize=(8, 4))

plt.subplot(1, 2, 1)

ax1 = sns.histplot(

x=ev['location'].str[0],

y=ev['location'].str[1],

bins=(20, 20), cmap="YlGnBu", cbar=True)

ax1.set_aspect('equal', 'box')

plt.title('España/Inglaterra 14/07/2024\n'

'Heatmap inicio de pases')

Realizamos lo mismo con los puntos donde se finalizaron los pases. Como puedes ver
usamos str[0] y str[1]. Estas son las coordenadas x e y en el campo.

plt.subplot(1, 2, 2)

ax2 = sns.histplot(

x=ev['pass_end_location'].str[0],

y=ev['pass_end_location'].str[1],

bins=(20, 20), cmap="YlGnBu", cbar=True)

ax2.set_aspect('equal', 'box')

plt.title('España/Inglaterra 14/07/2024\n'

'Heatmap fin de pases')

plt.tight_layout()
plt.show()

� Ejercicios:

• Realiza la misma representación para España y compáralo con el heatmap que acabamos de ver
de Inglaterra.

Estructuras de Control en Python

Antes de entrar a explicar las estructuras de control, vamos a ponernos un poco en contexto.

Un código es una secuencia de instrucciones, que por norma general son ejecutadas una tras otra.
Podemos verlo como una receta de cocina, en la que tenemos unos pasos a seguir. Empezamos por el
principio, vamos realizando cada tarea, y una vez hemos llegado al final, hemos terminado. Nuestra
receta en código podría ser la siguiente:

poner_agua_hervir()

echar_arroz_hervir()

cortar_pollo()

freir_pollo()

mezclar_pollo_arroz()

Sin embargo, en muchas ocasiones no basta con ejecutar las instrucciones una tras otra desde el
principio hasta llegar al final.

Puede ser que ciertas instrucciones se tengan que ejecutar si y sólo si se cumple una determinada
condición. ¿Que pasa si nuestro comensal es vegetariano? No hay problema, podemos usar el
condicional if. Si no es vegetariano usamos pollo, de lo contrario zanahoria.

poner_agua_hervir()
echar_arroz_hervir()

if not vegetariano:

cortar_pollo()

freir_pollo()

mezclar_pollo_arroz()

else:

cortar_zanahoria()

freir_zanahoria()

mezclar_zanahoria_arroz()

Por otro lado, la receta que teníamos era para una persona. ¿Qué pasa si queremos cocinar para 3?
¿Tenemos que escribir el código repetido 3 veces?

# Cocinamos para la primera persona

poner_agua_hervir()

echar_arroz_hervir()

cortar_pollo()

freir_pollo()

mezclar_pollo_arroz()

# Volvemos a cocinar para la segunda

poner_agua_hervir()

echar_arroz_hervir()

cortar_pollo()

freir_pollo()

mezclar_pollo_arroz()

# Y para la tercera

poner_agua_hervir()

echar_arroz_hervir()

cortar_pollo()
freir_pollo()

mezclar_pollo_arroz()

Pero ¿y si queremos para 100? Te puedes ya imaginar que repetir el código tantas veces no parece ser la
mejor idea. Es aquí donde entran en juego el for y el while.

Estas estructuras de control nos permiten repetir un determinado bloque de código tantas veces como
queramos.

# Repite lo que hay dentro 100 veces

for i in range(100):

# Cocinamos la receta

poner_agua_hervir()

echar_arroz_hervir()

cortar_pollo()

freir_pollo()

mezclar_pollo_arroz()

Como puedes ver, el código anterior haría lo mismo que copiar y pegar la receta 100 veces, pero es
mucho más compacto y elegante.

Sabido esto, ya estas en condiciones de empezar a leer este capítulo, donde aprenderás básico
conceptos como el if/else/for/while y también algo más avanzados, como lo son los iteradores, clases
iterables y uso del break/continue/try.

• 📗📗 Condicional if

• 📗📗 Bucle for

• 📗📗 Range

• 📗📗 Bucle while

• 📙📙 Switch

• 📙📙 Match

• 📙📙 Break

• 📙📙 Continue

• 📙📙 Iterar con zip

• 📙📙 Iterar con enumerate

• 📙📙 List comprehensions
• 📕📕 Iteradores e Iterables

Condicional en Python

De no ser por las estructuras de control, el código en cualquier lenguaje de programación sería
ejecutado secuencialmente hasta terminar. Un código, no deja de ser un conjunto de instrucciones que
son ejecutadas unas tras otra. Gracias a las estructuras de control, podemos cambiar el flujo de
ejecución de un programa, haciendo que ciertos bloques de código se ejecuten si y solo si se dan unas
condiciones particulares.

Uso del if

Un ejemplo sería si tenemos dos valores a y b que queremos dividir. Antes de entrar en el bloque de
código que divide a/b, sería importante verificar que b es distinto de cero, ya que la división por cero no
está definida. Es aquí donde entran los condicionales if.

a=4

b=2

if b != 0:

print(a/b)

En este ejemplo podemos ver como se puede usar un if en Python. Con el operador != se comprueba que
el número b sea distinto de cero, y si lo es, se ejecuta el código que está indentado. Por lo tanto
un if tiene dos partes:

• La condición que se tiene que cumplir para que el bloque de código se ejecute, en nuestro
caso b!=0.

• El bloque de código que se ejecutará si se cumple la condición anterior.

Es muy importante tener en cuenta que la sentencia if debe ir terminada por : y el bloque de código a
ejecutar debe estar indentado. Si usas algún editor de código, seguramente la indentación se producirá
automáticamente al presionar enter. Nótese que el bloque de código puede también contener más de
una línea, es decir puede contener más de una instrucción.

if b != 0:

c = a/b

d=c+1

print(d)

Todo lo que vaya después del if y esté indentado, será parte del bloque de código que se ejecutará si la
condición se cumple. Por lo tanto el segundo print() “Fuera if” será ejecutado siempre, ya que está fuera
del bloque if.

if b != 0:
c = a/b

print("Dentro if")

print("Fuera if")

Existen otros operadores que se verán en otros capítulos, como el de comparar si un número es mayor
que otro. Su uso es igual que el anterior.

if b > 0:

print(a/b)

Se puede también combinar varias condiciones entre el if y los :. Por ejemplo, se puede requerir que un
número sea mayor que 5 y además menor que 15. Tenemos en realidad tres operadores usados
conjuntamente, que serán evaluados por separado hasta devolver el resultado final, que será True si la
condición se cumple o False de lo contrario.

a = 10

if a > 5 and a < 15:

print("Mayor que 5 y menos que 15")

Es muy importante tener en cuenta que a diferencia de en otros lenguajes, en Python no puede haber un
bloque if vacío. El siguiente código daría un SyntaxError.

if a > 5:

Por lo tanto si tenemos un if sin contenido, tal vez porque sea una tarea pendiente que estamos dejando
para implementar en un futuro, es necesario hacer uso de pass para evitar el error. Realmente pass no
hace nada, simplemente es para tener contento al interprete de código.

if a > 5:

pass

Algo no demasiado recomendable pero que es posible, es poner todo el bloque que va dentro del if en la
misma línea, justo a continuación de los :. Si el bloque de código no es muy largo, puede ser útil para
ahorrarse alguna línea de código.

if a > 5: print("Es > 5")

Si tu bloque de código tiene más de una línea, se pueden poner también en la misma línea separándolas
con ;.

if a > 5: print("Es > 5"); print("Dentro del if")

Uso de else y elif

Es posible que no solo queramos hacer algo si una determinada condición se cumple, sino que además
queramos hacer algo de lo contrario. Es aquí donde entra la cláusula else. La parte del if se comporta de
la manera que ya hemos explicado, con la diferencia que si esa condición no se cumple, se ejecutará el
código presente dentro del else. Nótese que ambos bloque de código son excluyentes, se entra o en uno
o en otro, pero nunca se ejecutarán los dos.

x=5

if x == 5:

print("Es 5")

else:

print("No es 5")

Hasta ahora hemos visto como ejecutar un bloque de código si se cumple una instrucción, u otro si no
se cumple, pero no es suficiente. En muchos casos, podemos tener varias condiciones diferentes y para
cada una queremos un código distinto. Es aquí donde entra en juego el elif.

x=5

if x == 5:

print("Es 5")

elif x == 6:

print("Es 6")

elif x == 7:

print("Es 7")

Con la cláusula elif podemos ejecutar tantos bloques de código distintos como queramos según la
condición. Traducido al lenguaje natural, sería algo así como decir: si es igual a 5 haz esto, si es igual a 6
haz lo otro, si es igual a 7 haz lo otro.

Se puede usar también de manera conjunta todo, el if con el elif y un else al final. Es muy importante
notar que if y else solamente puede haber uno, mientras que elif puede haber varios.

x=5

if x == 5:

print("Es 5")

elif x == 6:

print("Es 6")

elif x == 7:

print("Es 7")

else:

print("Es otro")
Si vienes de otros lenguajes de programación, sabrás que el switch es una forma alternativa de elif, sin
embargo en Python esta cláusula no existe.

Operador ternario

El operador ternario o ternary operator es una herramienta muy potente que muchos lenguajes de
programación tienen. En Python es un poco distinto a lo que sería en C, pero el concepto es el mismo. Se
trata de una cláusula if, else que se define en una sola línea y puede ser usado por ejemplo, dentro de
un print().

Para saber más: El operador ternario fue propuesto en la PEP 308.

x=5

print("Es 5" if x == 5 else "No es 5")

#Es 5

Existen tres partes en un operador ternario, que son exactamente iguales a los que había en un if else.
Tenemos la condición a evaluar, el código que se ejecuta si se cumple, y el código que se ejecuta si no se
cumple. En este caso, tenemos los tres en la misma línea.

# [código si se cumple] if [condición] else [código si no se cumple]

Es muy útil y permite ahorrarse algunas líneas de código, además de aumentar la rapidez a la que
escribimos. Si por ejemplo tenemos una variable a la que queremos asignar un valor en función de una
condición, se puede hacer de la siguiente manera. Siguiendo el ejemplo anterior, en el siguiente código
intentamos dividir a entre b. Si b es diferente a cero, se realiza la división y se almacena en c, de lo
contrario se almacena -1. Ese -1 podría ser una forma de indicar que ha habido un error con la división.

a = 10

b=5

c = a/b if b!=0 else -1

print(c)

#2

Ejemplos if

# Verifica si un número es par o impar

x=6

if x % 2 == 0:

print("Es par")

else:

print("Es impar")
# Decrementa x en 1 unidad si es mayor que cero

x=5

x -= 1 if x > 0 else x

print(x)

Bucle for

A continuación explicaremos el bucle for y sus particularidades en Python, que comparado con otros
lenguajes de comparación, tiene ciertas diferencias.

El for es un tipo de bucle, parecido al while pero con ciertas diferencias. La principal es que el número de
iteraciones de un for esta definido de antemano, mientras que en un while no. La diferencia principal
con respecto al while es en la condición. Mientras que en el while la condición era evaluada en cada
iteración para decidir si volver a ejecutar o no el código, en el for no existe tal condición, sino
un iterable que define las veces que se ejecutará el código. En el siguiente ejemplo vemos un
bucle for que se ejecuta 5 veces, y donde la i incrementa su valor “automáticamente” en 1 en cada
iteración.

for i in range(0, 5):

print(i)

# Salida:

#0

#1

#2

#3

#4

Si has leído el capítulo del while, tal vez ya empieces a ver ventajas en el uso del for. Si por ejemplo,
queremos tener un número que va creciendo de 0 a n, hacerlo con for nos ahorra alguna línea de código,
porque no tenemos que escribir código para incrementar el número.

En Python se puede iterar prácticamente todo, como por ejemplo una cadena. En el siguiente ejemplo
vemos como la i va tomando los valores de cada letra. Mas adelante explicaremos que es esto de
los iterables e iteradores.

for i in "Python":

print(i)
# Salida:

#P

#y

#t

#h

#o

#n

Iterables e iteradores

Para entender al cien por cien los bucles for, y como Python fue diseñado como lenguaje de
programación, es muy importante entender los conceptos de iterables e iteradores. Empecemos con un
par de definiciones:

• Los iterables son aquellos objetos que como su nombre indica pueden ser iterados, lo que dicho
de otra forma es, que puedan ser indexados. Si piensas en un array (o una list en Python),
podemos indexarlo con lista[1] por ejemplo, por lo que sería un iterable.

• Los iteradores son objetos que hacen referencia a un elemento, y que tienen un método next que
permite hacer referencia al siguiente.

Para saber más: Si quieres saber más sobre los iteradores te dejamos este enlace a la documentación
oficial.

Ambos son conceptos un tanto abstractos y que pueden ser complicados de entender. Veamos unos
ejemplos. Como hemos comentado, los iterables son objetos que pueden ser iterados o accedidos con
un índice. Algunos ejemplos de iterables en Python son las listas, tuplas, cadenas o diccionarios.
Sabiendo esto, lo primero que tenemos que tener claro es que en un for, lo que va después del in deberá
ser siempre un iterable.

#for <variable> in <iterable>:

# <Código>

Tiene bastante sentido, porque si queremos iterar una variable, esta variable debe ser iterable, todo muy
lógico. Pero llegados a este punto, tal vez de preguntes ¿pero cómo se yo si algo es iterable o no?. Bien
fácil, con la siguiente función isinstance() podemos saberlo. No te preocupes si no entiendes muy bien lo
que estamos haciendo, fíjate solo en el resultado, True significa que es iterable y False que no lo es.

from collections import Iterable

lista = [1, 2, 3, 4]

cadena = "Python"

numero = 10
print(isinstance(lista, Iterable)) #True

print(isinstance(cadena, Iterable)) #True

print(isinstance(numero, Iterable)) #False

Por lo tanto las listas y las cadenas son iterables, pero numero, que es un entero no lo es. Es por eso por
lo que no podemos hacer lo siguiente, ya que daría un error. De hecho el error sería TypeError: int' object
is not iterable.

numero = 10

#for i in numero:

# print(i)

Una vez entendidos los iterables, veamos los iteradores. Para entender los iteradores, es importante
conocer la función iter() en Python. Dicha función puede ser llamada sobre un objeto que sea iterable, y
nos devolverá un iterador como se ve en el siguiente ejemplo.

lista = [5, 6, 3, 2]

it = iter(lista)

print(it) #<list_iterator object at 0x106243828>

print(type(it)) #<class 'list_iterator'>

Vemos que al imprimir it es un iterador, de la clase list_iterator. Esta variable iteradora, hace referencia a
la lista original y nos permite acceder a sus elementos con la función next(). Cada vez que llamamos
a next() sobre it, nos devuelve el siguiente elemento de la lista original. Por lo tanto, si queremos acceder
al elemento 4, tendremos que llamar 4 veces a next(). Nótese que el iterador empieza apuntando fuera
de la lista, y no hace referencia al primer elemento hasta que no se llama a next() por primera vez.

lista = [5, 6, 3, 2]

it = iter(lista)

print(next(it))

# [5, 6, 3, 2]

# ^

# |

# it

print(next(it))

# [5, 6, 3, 2]

# ^
# |

# it

print(next(it))

# [5, 6, 3, 2]

# ^

# |

# it

Para saber mas: Existen otros iteradores para diferentes clases:

• str_iterator para cadenas

• list_iterator para sets.

• tuple_iterator para tuplas.

• set_iterator para sets.

• dict_keyiterator para diccionarios.

Dado que el iterador hace referencia a nuestra lista, si llamamos más veces a next() que la longitud de la
lista, se nos devolverá un error StopIteration. Lamentablemente no existe ninguna opción de volver al
elemento anterior.

lista = [5, 6]

it = iter(lista)

print(next(it))

print(next(it))

#print(next(it)) # Error! StopIteration

Es perfectamente posible tener diferentes iteradores para la misma lista, y serán totalmente
independientes. Tan solo dependerán de la lista, como es evidente, pero no entre ellos.

lista = [5, 6, 7]

it1 = iter(lista)

it2 = iter(lista)

print(next(it1)) #5

print(next(it1)) #6

print(next(it1)) #7

print(next(it2)) #5
For anidados

Es posible anidar los for, es decir, meter uno dentro de otro. Esto puede ser muy útil si queremos iterar
algún objeto que en cada elemento, tiene a su vez otra clase iterable. Podemos tener por ejemplo, una
lista de listas, una especie de matriz.

lista = [[56, 34, 1],

[12, 4, 5],

[9, 4, 3]]

Si iteramos usando sólo un for, estaremos realmente accediendo a la segunda lista, pero no a los
elementos individuales.

for i in lista:

print(i)

#[56, 34, 1]

#[12, 4, 5]

#[9, 4, 3]

Si queremos acceder a cada elemento individualmente, podemos anidar dos for. Uno de ellos se
encargará de iterar las columnas y el otro las filas.

for i in lista:

for j in i:

print(j)

# Salida: 56,34,1,12,4,5,9,4,3

Ejemplos for

Iterando cadena al revés. Haciendo uso de [::-1] se puede iterar la lista desde el último al primer
elemento.

texto = "Python"

for i in texto[::-1]:

print(i) #n,o,h,t,y,P

Itera la cadena saltándose elementos. Con [::2] vamos tomando un elemento si y otro no.

texto = "Python"

for i in texto[::2]:

print(i) #P,t,o
Un ejemplo de for usado con comprehensions lists.

print(sum(i for i in range(10)))

# Salida: 45

Range en Python

Uso del range

Uno de las iteraciones mas comunes que se realizan, es la de iterar un número entre por ejemplo 0 y n. Si
ya programas, estoy seguro de que estas cansado de escribir esto, aunque sea en otro lenguaje.
Pongamos que queremos iterar una variable i de 0 a 5. Haciendo uso de lo que hemos visto
anteriormente, podríamos hacer lo siguiente.

for i in (0, 1, 2, 3, 4, 5):

print(i) #0, 1, 2, 3, 4, 5

Se trata de una solución que cumple con nuestro requisito. El contenido después del in se trata de una
clase que como ya hemos visto antes, es iterable, y es de hecho una tupla. Sin embargo, hay otras
formas de hacer esto en Python, haciendo uso del range().

for i in range(6):

print(i) #0, 1, 2, 3, 4, 5

El range() genera una secuencia de números que van desde 0 por defecto hasta el número que se pasa
como parámetro menos 1. En realidad, se pueden pasar hasta tres parámetros separados por coma,
donde el primer es el inicio de la secuencia, el segundo el final y el tercero el salto que se desea entre
números. Por defecto se empieza en 0 y el salto es de 1.

#range(inicio, fin, salto)

Por lo tanto, si llamamos a range() con (5,20,2), se generarán números de 5 a 20 de dos en dos. Un truco
es que el range() se puede convertir en list.

print(list(range(5, 20, 2)))

Y mezclándolo con el for, podemos hacer lo siguiente.

for i in range(5, 20, 2):

print(i) #5,7,9,11,13,15,17,19

Se pueden generar también secuencias inversas, empezando por un número mayor y terminando en uno
menor, pero para ello el salto deberá ser negativo.

for i in range (5, 0, -1):

print(i) #5,4,3,2,1
Bucles while

Anteriormente hemos visto el uso del if y el for para modificar el flujo de ejecución del código. A
continuación vemos otra forma de hacerlo con el while.

While

El uso del while nos permite ejecutar una sección de código repetidas veces, de ahí su nombre. El
código se ejecutará mientras una condición determinada se cumpla. Cuando se deje de cumplir, se
saldrá del bucle y se continuará la ejecución normal. Llamaremos iteración a una ejecución completa
del bloque de código.

Cabe destacar que existe dos tipos de bucles, los que tienen un número de iteraciones no definidas, y
los que tienen un número de iteraciones definidas. El while estaría dentro del primer tipo. Mas adelante
veremos los for, que se engloban en el segundo.

x=5

while x > 0:

x -=1

print(x)

# Salida: 4,3,2,1,0

En el ejemplo anterior tenemos un caso sencillo de while. Tenemos una condición x>0 y un bloque de
código a ejecutar mientras dure esa condición x-=1 y print(x). Por lo tanto mientras que x sea mayor que
0, se ejecutará el código. Una vez se llega al final, se vuelve a empezar y si la condición se cumple, se
ejecuta otra vez. En este caso se entra al bloque de código 5 veces, hasta que en la sexta, x vale cero y
por lo tanto la condición ya no se cumple. Por lo tanto el while tiene dos partes:

• La condición que se tiene que cumplir para que se ejecute el código.

• El bloque de código que se ejecutará mientras la condición se cumpla.

Ten cuidado ya que un mal uso del while puede dar lugar a bucles infinitos y problemas. Cierto es que en
algún caso tal vez nos interese tener un bucle infinito, pero salvo que estemos seguros de lo que
estamos haciendo, hay que tener cuidado. Imaginemos que tenemos un bucle cuya condición siempre
se cumple. Por ejemplo, si ponemos True en la condición del while, siempre que se evalúe esa expresión,
el resultado será True y se ejecutará el bloque de código. Una vez llegado al final del bloque, se volverá a
evaluar la condición, se cumplirá, y vuelta a empezar. No te recomiendo que ejecutes el siguiente
código, pero puedes intentarlo.

# No ejecutes esto, en serio

while True:

print("Bucle infinito")
Es posible tener un while en una sola línea, algo muy útil si el bloque que queremos ejecutar es corto. En
el caso de tener mas de una sentencia, las debemos separar con ;.

x=5

while x > 0: x-=1; print(x)

También podemos usar otro tipo de operación dentro del while, como la que se muestra a continuación.
En este caso tenemos una lista que mientras no este vacía, vamos eliminando su primer elemento.

x = ["Uno", "Dos", "Tres"]

while x:

x.pop(0)

print(x)

#['Dos', 'Tres']

#['Tres']

#[]

Else y while

Algo no muy corriente en otros lenguajes de programación pero si en Python, es el uso de la


cláusula else al final del while. Podemos ver el ejemplo anterior mezclado con el else. La sección de
código que se encuentra dentro del else, se ejecutará cuando el bucle termine, pero solo si lo hace “por
razones naturales”. Es decir, si el bucle termina porque la condición se deja de cumplir, y no porque se ha
hecho uso del break.

x=5

while x > 0:

x -=1

print(x) #4,3,2,1,0

else:

print("El bucle ha finalizado")

Podemos ver como si el bucle termina por el break, el print() no se ejecutará. Por lo tanto, se podría decir
que si no hay realmente ninguna sentencia break dentro del bucle, tal vez no tenga mucho sentido el uso
del else, ya que un bloque de código fuera del bucle cumplirá con la misma funcionalidad.

x=5

while True:

x -= 1
print(x) #4, 3, 2, 1, 0

if x == 0:

break

else:

# El print no se ejecuta

print("Fin del bucle")

Bucles anidados

Ya hemos visto que los bucles while tienen una condición a evaluar y un bloque de código a ejecutar.
Hemos visto ejemplos donde el bloque de código son operaciones sencillas como la resta -, pero
podemos complicar un poco mas las cosas y meter otro bucle while dentro del primero. Es algo que
resulta especialmente útil si por ejemplo queremos generar permutaciones de números, es decir, si
queremos generar todas las combinaciones posibles. Imaginemos que queremos generar todas las
combinaciones de de dos números hasta 2. Es decir, 0-0, 0-1, 0-2,… hasta 2-2.

# Permutación a generar

i=0

j=0

while i < 3:

while j < 3:

print(i,j)

j += 1

i += 1

j=0

Vamos a analizara el ejemplo paso por paso. El primer bucle genera números del 0 al 2, lo que
corresponde a la variable i. Por otro lado el segundo bucle genera también número del 0 al 2,
almacenados en la variable j. Al tener un bucle dentro de otro, lo que pasa es que por cada i se generan
3 j. Muy importante no olvidar que al finalizar el bucle de la j, debemos resetear j=0 para que en la
siguiente iteración la condición de entrada se cumpla.

Podemos complicar las cosas aún más y tener tres bucles anidados, generando combinaciones de 3
elementos con número 0, 1, 2. En este caso tendremos desde 0,0,0 hasta 2,2,2.

i, j, k = 0, 0, 0

while i < 3:

while j < 3:
while k < 3:

print(i,j,k)

k += 1

j += 1

k=0

i += 1

j=0

Ejemplos while

Árbol de navidad en Python. Imprime un árbol de navidad formado con * haciendo uso del while y de la
multiplicación de un entero por una cadena, cuyo resultado en Python es replicar la cadena.

z=7

x=1

while z > 0:

print(' ' * z + '*' * x + ' ' * z)

x+=2

z-=1

# *

# ***

# *****

# *******

# *********

# ***********

Aunque esta no sea tal vez la mejor forma de iterar una cadena es un buen ejemplo para el uso
del while e introducir el indexado de listas con [], que veremos en otros capítulos.

text = "Python"

i=0

while i < len(text):

print(text[:i + 1])

i += 1
#P

# Py

# Pyt

# Pyth

# Pytho

# Python

Sucesión de Fibonacci en Python. En matemáticas, la sucesión de fibonacci es una sucesión infinita de


números naturales, donde el siguiente es calculado sumando los dos anteriores.

a, b = 0, 1

while b < 25:

print(b)

a, b = b, a + b

#1, 1, 2, 3, 5, 8, 13, 21

Switch en Python

El switch es una herramienta que nos permite ejecutar diferentes secciones de código dependiendo de
una condición. Su funcionalidad es similar a usar varios if, pero por desgracia Python no tiene un
switch propiamente dicho. Sin embargo, hay formas de simular su comportamiento que te explicamos a
continuación. También ofrece el match, algo parecido que se introdujo en Python 3.10.

Introducción al switch

Ya sabemos que el uso del if junto con else y elif nos permite ejecutar un código determinado
dependiendo de una condicion, como podemos ver en el siguiente código.

if condicion == 1:

print("Haz a")

elif condicion == 2:

print("Haz b")

elif condicion == 3:

print("Haz c")

else:

print("Haz d")

La misma funcionalidad se podría escribir de la siguiente manera haciendo uso del switch. Como
puedes ver su uso tal vez resulte algo más limpio, y de hecho en determinadas ocasiones es más rápido.
// Ojo, esto no es Python

switch(condicion) {

case 1:

// haz a

break;

case 2:

// haz b

break;

case 3:

// haz c

break;

default:

// haz x

Pero tenemos un pequeño problema. En Python no existe el switch, por lo que si intentas ejecutar el
código anterior, tendrás un error.

Uno tal vez podría decir: bueno, que más da, uso if/elif/else y ya esta. La verdad que en la mayoría de los
casos, sería indiferente usar if o switch, pero si analizamos el comportamiento que existe por debajo,
funcionan de manera distinta. A pesar de que en Python no existe, te damos un truco que puede en
cierto modo emular su funcionamiento.

Diferencia if y switch

Una de las principales diferencias, es que usando if con elif, no todos los bloques tienen el mismo
tiempo de acceso. Todas las condiciones van siendo evaluadas hasta que se cumple y se sale.
Imaginemos que tenemos 100 condiciones.

if condicion == 1:

print("1")

elif condicion == 2:

print("2")

# ... hasta 100

elif condicion == 100:


print("3")

else:

print("x")

El tiempo de ejecución será distinto si la condicion es 1 o es 70 por ejemplo:

• Si es 1: Se evalúa el primer if, y como se cumple la condición se ejecuta y se sale.

• Si es 70: Se va evaluando cada condición, hasta llegar al 70. Es decir, tienen que evaluarse 70
condiciones.

Sin embargo, en el switch todos los elementos tienen el mismo tiempo de acceso. Esto se debe a que
por debajo esta normalmente implementado con lookup tables.

Si trabajamos con un gran número de condiciones, el uso del switch sobre el if podría notarse.

Dado que en Python no tenemos esta herramienta, te explicamos un truco para simularlo. No obstante,
si realmente te preocupan estas micro optimizaciones, tal vez no deberías usar Python.

Emulando switch en Python

Una forma de tener una especie de switch en Python es haciendo uso de un diccionario. Por lo tanto
podríamos convertir el siguiente código.

def opera1(operador, a, b):

if operador == 'suma':

return a + b

elif operador == 'resta':

return a - b

elif operador == 'multiplica':

return a * b

elif operador == 'divide':

return a / b

else:

return None

En el siguiente:

def opera2(operador, a, b):

return {
'suma': lambda: a + b,

'resta': lambda: a - b,

'multiplica': lambda: a * b,

'divide': lambda: a / b

}.get(operador, lambda: None)

Es importante notar que opera2 necesita de () para realizar la llamada a la función, ya que lo que se
devuelve en realidad es una función lambda.

opera1('suma', 5, 9)

# Salida: 14

opera2('suma', 5, 9)()

# Salida: 14

Tiempo Ejecución switch vs if

Como hemos indicado anteriormente, usar switch en vez de if puede ser más rápido. Aunque Python no
nos ofrece un switch propiamente dicho, lo podemos implementar con diccionarios. A continuación
veremos varios experimentos donde analizaremos el tiempo de ejecución de dos opciones distintas.

Empecemos con un problema a resolver. Imaginemos que queremos convertir números de decimal a
binario. Aunque esta no es la forma correcta de hacerlo, usaremos este ejemplo con fines didácticos.
Empecemos por definir las dos formas de hacerlo.

La primera forma es usando if y elif.

def usa_if(decimal):

if decimal == '0':

return "000"

elif decimal == '1':

return "001"

elif decimal == '2':

return "010"

elif decimal == '3':

return "011"

elif decimal == '4':


return "100"

elif decimal == '5':

return "101"

elif decimal == '6':

return "110"

elif decimal == '7':

return "111"

else:

return "NA"

La segunda forma es usando diccionarios simulando un switch.

tabla_switch = {

'0': '000',

'1': '001',

'2': '010',

'3': '011',

'4': '100',

'5': '101',

'6': '110',

'7': '111',

def usa_switch(decimal):

return tabla_switch.get(decimal, "NA")

Ambas funciones usa_if y usa_switch realizan lo mismo, pero están implementadas de manera distinta.
A continuación mediremos el tiempo de ejecución de ambas para saber cuál es más rápida. Vamos a
crear primero un decorador que nos permita medir el tiempo que una función tarda en ejecutarse.

import time

def mide_tiempo(funcion):

def funcion_medida(*args, **kwargs):

inicio = time.time()
c = funcion(*args, **kwargs)

print(f"Entrada: {args[1]}. Tiempo: {time.time() - inicio}")

return c

return funcion_medida

Y ahora vamos a crear una función que llame miles de veces a otra. Esto es debido a que necesitamos
realizar varias llamadas a la función para obtener un resultado fiable. Dado que si midiéramos una sola
ejecución de un if, a penas tardaríamos unos microsegundos.

@mide_tiempo

def repite_funcion(funcion, entrada):

return [funcion(entrada) for i in range(10000000)]

Ahora que ya tenemos todo lo que necesitamos para el experimento, vamos a llamar a nuestra funciones
con diferentes parámetros. Estos son los resultados:

for i in range(8):

repite_funcion(usa_if, str(i))

for i in range(8):

repite_funcion(usa_switch, str(i))

# Usando if:

# Entrada: 0. Tiempo: 2.167248249053955

# Entrada: 1. Tiempo: 2.4989960193634033

# Entrada: 2. Tiempo: 2.898904800415039

# Entrada: 3. Tiempo: 3.0172407627105713

# Entrada: 4. Tiempo: 3.7285051345825195

# Entrada: 5. Tiempo: 3.828972101211548

# Entrada: 6. Tiempo: 4.256570100784302

# Entrada: 7. Tiempo: 4.421134948730469

# Usando switch:

# Entrada: 0. Tiempo: 2.640446186065674


# Entrada: 1. Tiempo: 2.765054941177368

# Entrada: 2. Tiempo: 2.6275320053100586

# Entrada: 3. Tiempo: 2.561228036880493

# Entrada: 4. Tiempo: 2.5796279907226562

# Entrada: 5. Tiempo: 2.5939972400665283

# Entrada: 6. Tiempo: 2.5642037391662598

# Entrada: 7. Tiempo: 2.54691219329834

Por lo tanto, podemos concluir lo siguiente:

• Usando if el tiempo de ejecución no es siempre el mismo, ya que dependiendo del valor (0-7)
tendrán que evaluarse hasta 8 condiciones.

• Usando switch obtenemos un tiempo ligeramente mayor para el "0", pero el tiempo de ejecución
apenas varía cuando cambiamos la entrada.

Visto esto, no se puede concluir que una forma sea mejor que otra, todo dependerá del problema que
necesites resolver, pero la próxima vez que te enfrentes a un problema similar, ya tendrás las
herramientas para tomar una decisión razonada al respecto.

Sentencia break Python

Introducción al break

La sentencia break nos permite alterar el comportamiento de los bucles while y for. Concretamente,
permite terminar con la ejecución del bucle.

Esto significa que una vez se encuentra la palabra break, el bucle se habrá terminado.

Break con bucles for

Veamos como podemos usar el break con bucles for. El range(5) generaría 5 iteraciones, donde
la i valdría de 0 a 4. Sin embargo, en la primera iteración, terminamos el bucle prematuramente.

El break hace que nada más empezar el bucle, se rompa y se salga sin haber hecho nada.

for i in range(5):

print(i)

break

# No llega

# Salida: 0
Un ejemplo un poco más útil, sería el de buscar una letra en una palabra. Se itera toda la palabra y en el
momento en el que se encuentra la letra que buscábamos, se rompe el bucle y se sale.

Esto es algo muy útil porque si ya encontramos lo que estábamos buscando, no tendría mucho sentido
seguir iterando la lista, ya que desperdiciaríamos recursos.

cadena = 'Python'

for letra in cadena:

if letra == 'h':

print("Se encontró la h")

break

print(letra)

# Salida:

#P

#y

#t

# Se encontró la h

Break con bucles while

El break también nos permite alterar el comportamiento del while. Veamos un ejemplo.

La condición while True haría que la sección de código se ejecutara indefinidamente, pero al hacer uso
del break, el bucle se romperá cuando x valga cero.

x=5

while True:

x -= 1

print(x)

if x == 0:

break

print("Fin del bucle")

#4, 3, 2, 1, 0
Por norma general, y salvo casos muy concretos, si ves un while True, es probable que haya
un break dentro del bucle.

Break y bucles anidados

Como hemos dicho, el uso de break rompe el bucle, pero sólo aquel en el que está dentro.

Es decir, si tenemos dos bucles anidados, el break romperá el bucle anidado, pero no el exterior.

for i in range(0, 4):

for j in range(0, 4):

break

#Nunca se realiza más de una iteración

# El break no afecta a este for

print(i, j)

#00

#10

#20

#30

Sentencia continue

Introducción al continue

El uso de continue al igual que el ya visto break, nos permite modificar el comportamiento de de los
bucles while y for.

Concretamente, continue se salta todo el código restante en la iteración actual y vuelve al principio en el
caso de que aún queden iteraciones por completar.

La diferencia entre el break y continue es que el continue no rompe el bucle, si no que pasa a la siguiente
iteración saltando el código pendiente.

En el siguiente ejemplo vemos como al encontrar la letra P se llama al continue, lo que hace que se salte
el print(). Es por ello por lo que no vemos la letra P impresa en pantalla.

cadena = 'Python'

for letra in cadena:

if letra == 'P':

continue
print(letra)

# Salida:

#y

#t

#h

#o

#n

A diferencia del break, el continue no rompe el bucle sino que finaliza la iteración actual, haciendo que
todo el código que va después se salte, y se vuelva al principio a evaluar la condición.

En el siguiente ejemplo podemos ver como cuando la x vale 3, se llama al continue, lo que hace que se
salte el resto de código de la iteración (el print()). Por ello, vemos como el número 3 no se imprime en
pantalla.

x=5

while x > 0:

x -= 1

if x == 3:

continue

print(x)

#Salida: 4, 2, 1, 0

Iterar con zip en Python

La función zip() de Python viene incluida por defecto en el namespace, lo que significa que puede ser
usada sin tener que importarse.

De acuerdo con la documentación oficial:

Returns an iterator of tuples, where the i-th tuple contains the i-th element from each of the argument
sequences or iterables. The iterator stops when the shortest input iterable is exhausted.

Dicho de otra manera, si pasamos dos listas a zip como entrada, el resultado será una tupla donde cada
elemento tendrá todos y cada uno de los elementos i-ésimos de las pasadas como entrada.

Veamos un ejemplo. Como podemos ver, el resultado tras aplicar zip es una lista con a[0]b[0] en el
primer elemento y a[1]b[1] como segundo.

a = [1, 2]
b = ["Uno", "Dos"]

c = zip(a, b)

print(list(c))

# [(1, 'Uno'), (2, 'Dos')]

A priori puede parecer una función no muy relevante, pero es realmente útil combinada con un for para
iterar dos listas en paralelo.

a = [1, 2]

b = ["Uno", "Dos"]

c = zip(a, b)

for numero, texto in zip(a, b):

print("Número", numero, "Letra", texto)

# Número 1 Letra Uno

# Número 2 Letra Dos

zip() con n argumentos

Ya hemos visto el uso de zip con dos listas, pero dado que está definida como zip(*iterables), es posible
pasar un número arbitrario de iterables como entrada.

Veamos un ejemplo con varias listas. Es importante notar que todas tienen la misma longitud, dos.

numeros = [1, 2]

espanol = ["Uno", "Dos"]

ingles = ["One", "Two"]

frances = ["Un", "Deux"]

c = zip(numeros, espanol, ingles, frances)

for n, e, i, f in zip(numeros, espanol, ingles, frances):

print(n, e, i, f)
# 1 Uno One Un

# 2 Dos Two Deux

zip() con diferentes longitudes

También podemos usar zip usando iterables de diferentes longitudes. En este caso lo que pasará es que
el iterador para cuando la lista más pequeña se acaba.

numeros = [1, 2, 3, 4, 5]

espanol = ["Uno", "Dos"]

for n, e in zip(numeros, espanol):

print(n, e)

# 1 Uno

# 2 Dos

Resulta lógico que este sea el comportamiento, porque de no ser así y se continuara, no tendríamos
valores para usar.

zip() con un argumento

Como cabría esperar, dado que zip está definido para un número arbitrario de parámetros de entrada, es
posible también posible usar un único valor. El resultado son tuplas de un elemento.

numeros = [1, 2, 3, 4, 5]

zz = zip(numeros)

print(list(zz))

# [(1,), (2,), (3,), (4,), (5,)]

zip() con diccionarios

Hasta ahora nos hemos limitado a usar zip con listas, pero la función está definida para cualquier
clase iterable. Por lo tanto podemos usarla también con diccionarios.

Si realizamos lo siguiente, a,b toman los valores de las key del diccionario. Tal vez algo no demasiado
interesante.

esp = {'1': 'Uno', '2': 'Dos', '3': 'Tres'}

eng = {'1': 'One', '2': 'Two', '3': 'Three'}


for a, b in zip(esp, eng):

print(a, b)

#11

#22

#33

Sin embargo, si hacemos uso de la función items, podemos acceder al key y value de cada elemento.

esp = {'1': 'Uno', '2': 'Dos', '3': 'Tres'}

eng = {'1': 'One', '2': 'Two', '3': 'Three'}

for (k1, v1), (k2, v2) in zip(esp.items(), eng.items()):

print(k1, v1, v2)

# 1 Uno One

# 2 Dos Two

# 3 Tres Three

Nótese que en este caso ambas key k1 y k2 son iguales.

Deshacer el zip()

Con un pequeño truco, es posible deshacer el zip en una sola línea de código. Supongamos que hemos
usado zip para obtener c.

a = [1, 2, 3]

b = ["One", "Two", "Three"]

c = zip(a, b)

print(list(c))

# [(1, 'One'), (2, 'Two'), (3, 'Three')]

¿Es posible obtener a y b desde c? Sí, tan sencillo como:

c = [(1, 'One'), (2, 'Two'), (3, 'Three')]


a, b = zip(*c)

print(a) # (1, 2, 3)

print(b) # ('One', 'Two', 'Three')

Nótese el uso de *c, lo que es conocido como unpacking en Python.

Enumerate en Python

El uso del for en Python nos permite iterar colecciones, recorriendo todos los elementos de la misma.

lista = ["A", "B", "C"]

for l in lista:

print(l)

# Salida:

#A

#B

#C

Sin embargo, existen situaciones en las que no solo queremos acceder al elemento i-ésimo de la
colección, sino que además queremos el índice. Una forma de hacerlo sería la siguiente.

lista = ["A", "B", "C"]

indice = 0

for l in lista:

print(indice, l)

indice += 1

# Salida:

#0A

#1B

#2C
Aunque se trata de una forma perfectamente válida, no es demasiado pythónica, y es precisamente
donde entra en juego el enumerate(). Su uso nos permite ahorrar alguna que otra línea de código,
obteniendo un resultado mucho más limpio y claro.

lista = ["A", "B", "C"]

for indice, l in enumerate(lista):

print(indice, l)

# Salida:

#0A

#1B

#2C

Por último, es importante notar que su uso no se limita únicamente a bucles for. Podemos convertir el
tipo enumerate en una lista de tuplas, donde cada una contiene un elemento de la colección inicial y el
índice asociado.

lista = ["A", "B", "C"]

en = list(enumerate(lista))

print(en)

# Salida;

# [(0, 'A'), (1, 'B'), (2, 'C')]

Por lo tanto recuerda, la próxima vez que quieras acceder a los índices de una colección, piensa si tal
vez enumerate puede resolver tu problema de manera más clara y con menos código.

List comprehensions

Una de las principales ventajas de Python es que una misma funcionalidad puede ser escrita de
maneras muy diferentes, ya que su sintaxis es muy rica en lo que se conoce como expresiones
idiomáticas o idiomatic expressions. Las list comprehension o comprensión de listas son una de ellas.

Vayamos al grano, las list comprehension nos permiten crear listas de elementos en una sola línea de
código. Por ejemplo, podemos crear una lista con los cuadrados de los primeros 5 números de la
siguiente forma

cuadrados = [i**2 for i in range(5)]


#[0, 1, 4, 9, 16]

De no existir, podríamos hacer lo mismo de la siguiente forma, pero necesitamos alguna que otra línea
más de código.

cuadrados = []

for i in range(5):

cuadrados.append(i**2)

#[0, 1, 4, 9, 16]

El resultado es el mismo, pero resulta menos claro. Antes de continuar, veamos la sintaxis general de las
comprensiones de listas.

# lista = [expresión for elemento in iterable]

Es decir, por un lado tenemos el for elemento in iterable, que itera un determinado iterable y “almacena”
cada uno de los elementos en elemento como vimos en este otro post sobre el for. Por otro lado,
tenemos la expresión, que es lo que será añadido a la lista en cada iteración.

La expresión puede ser una operación como hemos visto anteriormente i**2, pero también puede ser un
valor constante. El siguiente ejemplo genera una lista de cinco unos.

unos = [1 for i in range(5)]

#[1, 1, 1, 1, 1]

La expresión también puede ser una llamada a una función. Se podría escribir el ejemplo anterior del
cálculo de cuadrados de la siguiente manera.

def eleva_al_2(i):

return i**2

cuadrados = [eleva_al_2(i) for i in range(5)]

#[0, 1, 4, 9, 16]

Como puedes observar, las posibilidades son bastante amplias. Cualquier elemento que sea iterable
puede ser usado con las list comprehensions. Anteriormente hemos iterado range() pero podemos hacer
lo mismo para una lista. En el siguiente ejemplo vemos como dividir todos los números de una lista entre
10.

lista = [10, 20, 30, 40 , 50]

nueva_lista = [i/10 for i in lista]

#[1.0, 2.0, 3.0, 4.0, 5.0]

Añadiendo condicionales
En el apartado anterior hemos visto como modificar todos los elementos de un iterable (como una lista)
de diferentes maneras. La primera elevando cada elemento al cuadrado, y la segunda dividiendo cada
elemento por diez.

Pero, ¿y si quisiéramos realizar la operación sobre el elemento sólo si una determinada condición se
cumple? Pues tenemos buenas noticias, porque es posible añadir un condicional if. La expresión
genérica sería la siguiente.

# lista = [expresión for elemento in iterable if condición]

Por lo tanto la expresión sólo se aplicará al elemento si se cumple la condición. Veamos un ejemplo con
una frase, de la que queremos saber el número de erres que tiene.

frase = "El perro de san roque no tiene rabo"

erres = [i for i in frase if i == 'r']

#['r', 'r', 'r', 'r']

Lo que hace el código anterior es iterar cada letra de la frase, y si es una r, se añade a la lista. De esta
manera el resultado es una lista con tantas letras r como la frase original tiene, y podemos calcular las
veces que se repite con len().

print(len(erres))

#4

Sets comprehension

Las set comprehensions son muy similares a las listas que hemos visto con anterioridad. La única
diferencia es que debemos cambiar el () por {}. Como resulta evidente, dado que los set no permiten
duplicados, si intentamos añadir un elemento que ya existe en el set, simplemente no se añadirá.

frase = "El perro de san roque no tiene rabo"

mi_set = {i for i in frase if i == "r"}

#{'r'}

Dictionary comprehension

Y por último, también tenemos las comprensiones de diccionarios. Son muy similares a las anteriores,
con la única diferencia que debemos especificar la key o llave. Veamos un ejemplo.

lista1 = ['nombre', 'edad', 'región']

lista2 = ['Pelayo', 30, 'Asturias']

mi_dict = {i:j for i,j in zip(lista1, lista2)}

#{'nombre': 'Pelayo', 'edad': 30, 'región': 'Asturias'}


Como se puede ver, usando : asignamos un valor a una llave. Hemos usado también zip(), que nos
permite iterar dos listas paralelamente. Por lo tanto, en este ejemplo estamos convirtiendo dos listas a
un diccionario.

Conclusiones

Las comprensiones de listas, sets o diccionarios son una herramienta muy útil para hacer que nuestro
código resulte más compacto y fácil de leer. Siempre que tengamos una colección iterable que
queramos modificar, son una buena opción para evitar tener que escribir bucles for.

Las comprensiones están también muy relacionadas con el concepto de programación funcional y otra
funciones que Python nos ofrece como filter o map, por lo que si no las conoces te recomendamos que
leas sobre ellas.

En ciertas ocasiones, las comprensiones no resultan sólo útiles por que puedan ser escritas en una sola
línea de código, sino que también pueden llegar a ser más rápidas que otros métodos. Es muy
importante por lo tanto medir su tiempo de ejecución para saber si son una buena elección.

Y por último, aunque su uso resulte de lo más Pythónico y elegante (algo que a muchos programadores
de Python les encanta), hay que tener cuidado con su uso y no abusar de ellas. Resulta fácil caer en la
tentación de acabar escribiendo comprensiones que son tan largas que prácticamente son imposibles
de leer, algo que puede no ser muy buena idea.

Iteradores e Iterables

Si ya entiendes el uso del while y el for, entonces sin duda estás listo para continuar con los iterables.
Sin duda son una herramienta muy potente de Python que nos permite como su nombre
indica, iterar colecciones que sean iterables. A continuación veremos estos dos conceptos en detalle.

Antes de nada planteemos el problema que queremos resolver. Tenemos una determinada colección de
datos, en este caso una lista con varios valores, y queremos mostrar sus valores uno a uno por pantalla.
Si eres nuevo en Python o vienes de otros lenguajes de programación, tal vez lo resolverías de la
siguiente manera con un while.

# Mal uso

lista = [5, 4, 9, 2]

i=0

while i < len(lista):

elemento = lista[i]

print(elemento)

i += 1

# Salida: 5, 4, 9, 2
Aunque es una solución válida y que funciona perfectamente, tal vez sea mejor usar un bucle for, ya que
nos podemos ahorrar alguna línea de código.

# Mal uso

lista = [5, 4, 9, 2]

for i in range(len(lista)):

elemento = lista[i]

print(elemento)

# Salida: 5, 4, 9, 2

Aunque esta segunda forma es también válida, en Python existe una forma mucho más fácil de iterar una
lista. Dicha forma es la siguiente.

lista = [5, 4, 9, 2]

for elemento in lista:

print(elemento)

# Salida 5, 4, 9, 2

Si saberlo, ya has hecho uso de los iteradores, usando la clase lista que es una clase iterable. Como
puedes ver, se trata de una solución mucho más sencilla. A continuación veremos lo que es un iterable y
cómo puede ser usado.

Iterables

Una clase iterable es una clase que puede ser iterada. Dentro de Python hay gran cantidad de clases
iterables como las listas, strings, diccionarios o ficheros. Si tenemos una clase iterable, podemos usarla
a la derecha del for de la siguiente manera.

# for elemento in [clase_iterable]:

# ...

Si usamos el for como acabamos de mostrar, la variable elemento irá tomando los valores de cada
elemento presente en la clase iterable. De esta manera, ya no tenemos que ir accediendo manualmente
con [] a cada elemento.

Anteriormente hemos visto un ejemplo iterando una lista, pero también podemos iterar una cadena, ya
que es una clase iterable. Al iterar una cadena se nos devuelve cada letra presente en la misma. Como
puedes ver, la sintaxis se asemeja bastante al lenguaje natural, sería algo así como decir “pon en c cada
elemento presenta en la cadena”.

cadena = "Hola"

for c in cadena:
print(c)

# Salida: H o l a

Llegados a este punto, tal vez te preguntes ¿y cómo se yo si una clase es iterable o no? Pues bien, tienes
dos opciones. La primera sería consultar la documentación oficial de Python. La segunda es ver si la
clase u objeto en cuestión hereda de Iterable (aquí te explicamos la herencia por si aún no la tienes
clara). Con isinstance() podemos comprobar si una clase hereda de otra.

from collections import Iterable

cadena = "Hola"

numero = 3

print("cadena", isinstance(cadena, Iterable))

print("numero", isinstance(numero, Iterable))

# Salida

# cadena True

# numero False

Podemos ver como efectivamente la cadena es iterable y el número no. Es por ello por lo que podemos
iterar la cadena, pero el siguiente código daría un error.

numero = 3

for x in numero:

print(x)

# Error TypeError: 'int' object is not iterable

Python nos ofrece también diferentes métodos que pueden ser usados sobre clases iterables como los
que se muestran a continuación:

• list() convierte a lista una clase iterable

• sum() para sumar

• join() permite unir cada elemento de una clase iterable con el primer argumento usado.

print(list("Hola"))

print(sum([1, 2, 3]))

print("-".join("Hola"))
# Salida:

#['H', 'o', 'l', 'a']

#6

#H-o-l-a

De la misma forma que iteramos una cadena o una lista, también podemos iterar un diccionario. El
iterador del diccionario devuelve las claves o keys del mismo.

mi_dict = {'a':1, 'b':2, 'c':3}

for i in mi_dict:

print(i)

# Salida: a, b, c

Una vez hemos entendido lo que es una clase iterable, veamos lo que es un iterador.

Iteradores

Se podría explicar la diferencia entre iteradores e iterables usando un libro como analogía. El libro sería
nuestra clase iterable, ya que tiene diferentes páginas a las que podemos acceder. El libro podría ser una
lista, y cada página un elemento de la lista. Por otro lado, el iterador sería un marcapáginas, es decir, una
referencia que nos indica en qué posición estamos del libro, y que puede ser usado para “navegar” por
él.

Es posible obtener un iterador a partir de una clase iterable con la función iter(). En el siguiente ejemplo
podemos ver como obtenemos el iterador del libro.

libro = ['página1', 'página2', 'página3', 'página4']

marcapaginas = iter(libro)

Llegados a este punto, nuestro marcapaginas almacena un iterador. Se trata de un objeto que podemos
usar para navegar a través del libro. Usando la función next() sobre el iterador, podemos ir accediendo
secuencialmente a cada elemento de nuestra lista (las páginas de libro).

print(next(marcapaginas))

print(next(marcapaginas))

print(next(marcapaginas))

print(next(marcapaginas))

# página1
# página2

# página3

# página4

Algo parecido a esto es lo que sucede por debajo cuando usamos el for sobre una clase iterable. Se va
accediendo secuencialmente a los elementos hasta que la excepción StopIteration es lanzada. Dicha
excepción se lanza cuando hemos llegado al final, y no existen más elementos que iterar.

print(next(marcapaginas))

# Salida: StopIteration

Una nota muy importante es que cuando el iterador es obtenido con iter() como hemos visto, apunta por
defecto fuera de la lista. Es decir, si queremos acceder al primer elemento de la lista, deberemos llamar
una vez a next().

Por otro lado, a diferencia de un marcapáginas de un libro, el iterador sólo puede ir hacia delante. No es
posible retroceder.

Creando tu clase iterable

Llegados a este punto ya entendemos perfectamente los iterables e iteradores y hemos visto como
pueden ser usados con diferentes clases de Python como las cadenas o listas. Sin embargo, tal vez
quieras dar un paso más y definir tu propia clase. En este post ya te explicamos cómo definir tus clases.
A continuación te explicaremos cómo hacer que tu clase sea iterable.

Empecemos desde cero. Vamos a definir una clase MiClase y crear un objeto con ella. Si intentamos
usar la función iter() para obtener su iterador, tendremos un error ya que nuestra clase por defecto no es
iterable.

class MiClase:

pass

miobjeto = MiClase()

iterador = iter(miobjeto)

# Salida

# TypeError: 'MiClase' object is not iterable

Para poder llamar a la función iter() sobre la clase, debemos implementar el método dunder __iter__().
Dicho método debe devolver un iterable, que será usado cuando la clase intente ser iterada.

class MiClase:
def __init__(self, items):

self.lista = items

def __iter__(self):

return iter(self.lista)

Podemos ver como tenemos el método __init__() que es llamado cuando se crea una nueva instancia de
la clase. Simplemente pasamos una lista como parámetro de entrada y la almacenamos como atributo
en .lista.

Por otro lado, el método __iter__() devuelve un iterador, que simplemente es el iterador de la propia lista.
Ahora que nuestra clase ya es iterable, podemos hacer lo siguiente.

miobjeto = MiClase([5, 4, 3])

for item in miobjeto:

print(item)

# Salida: 5, 4, 3

Cabe destacar que el ejemplo mostrado tiene fines didácticos y poca aplicación práctica, ya que
simplemente se está encapsulando una lista dentro de una clase. Sin embargo sirve para ejemplificar
cómo una clase se puede convertir en iterable, y seguramente con esta base encuentres aplicaciones
prácticas en tus proyectos.

TIPOS DE ESTRUCTURA

• 📗📗 Entero o int

• 📗📗 Booleano

• 📗📗 Float

• 📗📗 Números complejos

• 📗📗 Cadenas o strings

• 📗📗 Listas

• 📙📙 Set

• 📙📙 Tupla o tuple

• 📙📙 Diccionario

• 📙📙 Frozenset

• 📙📙 Castings
• 📙📙 Colecciones

• 📙📙 Mutabilidad

Entero o int
Los enteros en Python o también conocidos como int, son un tipo de datos que permite representar
números enteros, es decir, positivos y negativos no decimales. Si vienes de otros lenguajes de
programación, olvídate de los int8 , uint16 y demás. Python nos da un sólo tipo que podemos usar
sin preocuparnosdel tamaño del número almacenado por debajo.

Introducción a int
Los tipos enteros o int en Python permiten almacenar un valor numérico no decimal ya sea
positivo o negativo de cualquier valor. La función type() nos devuelve el tipo de la variable, y
podemos ver com efectivamente es de la clase int .
i = 12
print(i) #12
print(type(i)) #<class 'int'>

En otros lenguajes de programación, los int tenían un valor máximo que pueden representar.
Dependiendo de la longitud de palabra o wordsize de la arquitectura del ordenador, existen unos
números mínimos y máximos que era posible representar. Si por ejemplo se usan enteros de 32 bits
el rango a representar es de -2^31 a 2^31–1, es decir, -2.147.483.648 a 2.147.483.647. Con 64 bits,
el rango es de -9.223.372.036.854.775.808 hasta 9.223.372.036.854.775.807. Una gran ventaja de
Python es que ya no nos tenemos que preocupar de esto, ya que por debajo se encarga de asignar
más o menos memoria al número, y podemos representar prácticamente cualquier número.
x = 250**250
print(x)
print(type(x))
#3054936363499604682051979393213617699789402740572326663893613909281291626524720457701857235108015
22825687515269359046715531785342780428396973513311420091788963072442053377285222203558881953188370
08165086679301794879136633899370525163649789227021200352450820912190874482021196014946372110934030
79855076782836518362040933993739599827677011489868164062500000000000000000000000000000000000000000
00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
0000000000000
#<class 'int'>

Para saber más: Si vienes de otras versiones de Python, tal vez eches de menos el tipo long. Puedes
leer la PEP237, donde se explica porqué se unificó los long y los int.
Ejemplos
En el primer ejemplo hemos asignado un número decimal a una variable, pero también es posible
asignar valores en binario , hexadecimal y octal . El prefijo 0b indica que lo que viene a
continuación será interpretado como un número binario. Para el caso hexadecimal es con 0x y
octal con 0c . Al imprimir, el número se convierte en decimal para todos los casos.
a = 0b100
b = 0x17
c = 0o720
print(a, type(a)) #4 <class 'int'>
print(b, type(b)) #23 <class 'int'>
print(c, type(c)) #464 <class 'int'>

Con el siguiente ejemplo podemos ver como Python asigna diferente número de bits a las
variables en función del número que quieren representar. La función getsizeof() devuelve el
tamaño de una variable en memoria.
import sys
x = 5**10000
y = 10
print(sys.getsizeof(x), type(x))
print(sys.getsizeof(y), type(y))

Aunque como hemos dicho, Python puede representar casi cualquier número, hay casos límite en
los que nos podemos encontrar con una excepción como la que mostramos a continuación.
print(5e200**2)
# OverflowError

Un caso curioso es que si intentamos representar un número aún mayor, nos encontraremos con
lo siguiente en vez de con una excepción.
print(2e2000**2)
# inf

Convertir a int
El posible convertir a int otro tipo. Como hemos explicado, el tipo int no puedo contener
decimales, por lo que si intentamos convertir un número decimal, se truncará todo lo que tengamos
a la derecha de la coma.
b = int(1.6)
print(b) #1
Booleanos en Python

Al igual que en otros lenguajes de programación, en Python existe el tipo bool o booleano. Es un tipo de
dato que permite almacenar dos valores True o False.

Declarar variable bool

Se puede declarar una variable booleana de la siguiente manera.


x = True

y = False

Evaluar expresiones

Un valor booleano también puede ser el resultado de evaluar una expresión. Ciertos operadores como el
mayor que, menor que o igual que devuelven un valor bool.

print(1 > 0) #True

print(1 <= 0) #False

print(9 == 9) #True

Función bool

También es posible convertir un determinado valor a bool usando la función bool().

print(bool(10)) # True

print(bool(-10)) # True

print(bool("Hola")) # True

print(bool(0.1)) # True

print(bool([])) # False

Uso con if

Los condicionales if evalúan una condición que es un valor bool.

a=1

b=2

if b > a:

print("b es mayor que a")

La expresión que va después del if es siempre evaluada hasta que se da con un booleano.

if True:

print("Es True")

Bool como subclase de int

Es importante notar que aunque estemos listando el tipo bool como si fuese un tipo más, es en realidad
una subclase del int visto anteriormente. De hecho lo puedes comprobar de la siguiente manera.

isinstance(True, int)

#True
issubclass(bool, int)

#True

Float

El tipo numérico float permite representar un número positivo o negativo con decimales, es decir,
números reales. Si vienes de otros lenguajes, tal vez conozcas el tipo doble, lo que significa que tiene el
doble de precisión que un float. En Python las cosas son algo distintas, y los float son en realidad double.

Para saber más: Los valores float son almacenados de una forma muy particular, denominada
representación en coma flotante. En el estándar IEEE 754 se explica con detalle.

Por lo tanto si declaramos una variable y le asignamos un valor decimal, por defecto la variable será de
tipo float.

f = 0.10093

print(f) #0.10093

print(type(f)) #<class 'float'>

Conversión a float

También se puede declarar usando la notación científica con e y el exponente. El siguiente ejemplo sería
lo mismo que decir 1.93 multiplicado por diez elevado a -3.

f = 1.93e-3

También podemos convertir otro tipo a float haciendo uso de float(). Podemos ver como True es en
realidad tratado como 1, y al convertirlo a float, como 1.0.

a = float(True)

b = float(1)

print(a, type(a)) #1.0 <class 'float'>

print(b, type(b)) #1.0 <class 'float'>

Rango representable

Alguna curiosidad es que los float no tienen precisión infinita. Podemos ver en el siguiente ejemplo como
en realidad f se almacena como si fuera 1, ya que no es posible representar tanta precisión decimal.

f = 0.99999999999999999

print(f) #1.0

print(1 == f) #True

Los float a diferencia de los int tienen unos valores mínimo y máximos que pueden representar. La
mínima precisión es 2.2250738585072014e-308 y la máxima 1.7976931348623157e+308, pero si no nos
crees, lo puedes verificar tu mismo.
import sys

print(sys.float_info.min) #2.2250738585072014e-308

print(sys.float_info.max) #1.7976931348623157e+308

De hecho si intentas asignar a una variable un valor mayor que el max, lo que ocurre es que esa variable
toma el valor inf o infinito.

f = 1.7976931348623157e+310

print(f) #inf

Si quieres representar una variable que tenga un valor muy alto, también puedes crear directamente una
variable que contenga ese valor inf.

f = float('inf')

print(f) #inf

Precisión del float

Desafortunadamente, los ordenadores no pueden representar cualquier número, y menos aún si este es
uno irracional. Debido a esto, suceden cosas curiosas como las siguientes.

Dividir 1/3 debería resultar en 0.3 periódico, pero para Python es imposible representar tal número.

print("{:.20f}".format(1.0/3.0))

# 0.33333333333333331483

Por otro lado, la siguiente operación debería tener como resultado cero, pero como puedes ver esto no
es así.

print(0.1 + 0.1 + 0.1 - 0.3)

# 5.551115123125783e-17

Afortunadamente, salvo aplicaciones muy concretas esto no resulta un problema. Al fin y al cabo, ¿a


quién le importa lo que haya en la posición decimal 15?

¿Qué son los números complejos?

En pocas palabras, los números complejos son aquellos que tienen dos partes:

• Una parte real, como por ejemplo 3 o 7.5.

• Y otra imaginaria, como 5j o -7j.

Como puedes ver, la parte imaginaria viene acompañada de j, aunque es también común usar la i (de
imaginario). Un número complejo tiene la siguiente forma:

parte_real + parte_imaginaria*j

Veamos unos ejemplos:


a = 5 + 5j

b = 1.3 - 7j

También podemos tener un número complejo con parte real igual a cero.

c = 10.3j

Una vez vistos cómo son, vamos a ponerlos en contexto con el resto de conjuntos numéricos:

• Los números enteros hacen referencia a los números naturales, es decir, todos aquellos números
que no contienen parte decimal: 3, -1, 10.

• Los números racionales son el cociente de dos números naturales: 3/10, 7/9.

• Los números irracionales son aquellos que no pueden ser expresados como una fracción m/n.
Por ejemplo, el número pi es irracional.

• Los números reales son el conjunto de todos los anteriores, es decir, cualquier número que se te
ocurra sería un número real.

• Los números imaginarios son números reales acompañados de la constante i (a veces la j es


usada indistintamente), como 4i o 3.7i.

• Y por último los números complejos son la suma de un número real y otro imaginario, dando
lugar a los ya vistos 5+5j.

Llegados a este punto, tal vez te preguntes ¿y qué es i? Pues bien, i es simplemente un nombre que se le
da a la raíz cuadrada de -1, ya que si recuerdas, los números negativos no tienen raíces cuadradas. Esta
notación se la debemos al famoso Leonhard Euler.

Por lo tanto, la raíz cuadrada de -5, podría expresarse como 5i, y aunque pueda parecer algo poco
relevante, se trata de una herramienta muy potente en el mundo de las matemáticas, física e ingeniería.

¿Para qué sirven los números complejos?

Los números complejos son muy utilizados en las telecomunicaciones y electrónica, ya que son muy
útiles para describir las ondas electromagnéticas y la corriente eléctrica.

También son usados en diferentes dominios de las matemáticas y en física, donde destaca su uso en la
mecánica cuántica.

Un número complejo puede representarse en un plano, donde el eje x representa la parte real y el eje y la
imaginaria. Es decir, se puede ver a un número complejo 5+5i como un punto de coordenadas.
Una vez representado en esta gráfica, cualquier número complejo formará un ángulo con el eje x. Todo
número complejo tiene también un módulo, que es la distancia que une el punto del origen de
coordenadas 0+0i.

Ahora sólo tienes que imaginarte a este punto dando vueltas a una determinada frecuencia alrededor del
plano, y ya estarías describiendo a una onda sin haberte dado cuenta.

Números complejos en Python

Los números complejos en Python pueden ser creados sin tener que importar ninguna librería. Basta con
hacer lo siguiente:

c = 3 + 5j

print(c) #(3+5j)

print(type(c)) #<class 'complex'>

Podemos ver como la clase que representa a los complejos en Python se llama complex. Una vez
creado, es posible acceder a la parte real con real y a la imaginaria con imag.

c = 3 + 5j

print(c.real) #3.0

print(c.imag) #5.0

También se puede crear un número complejo haciendo uso de complex, pero sin usar la j.

c = complex(3,5)

print(c) #(3+5j)

Operaciones con números complejos

Usando variables del tipo complex, podemos realizar las operaciones más comunes típicas de los
números complejos.

Suma de complejos
Para sumar números complejos, se suman las partes reales por un lado, y las imaginarias por otro.

a = 1 + 3j

b = 4 + 1j

print(a+b) #(5+4j)

Resta de complejos

Para restar, se restan las partes reales por un lado y las imaginarias por otro.

a = 1 + 3j

b = 4 + 1j

print(a-b) #(-3+2j)

Multiplicación de complejos

La multiplicación es algo más compleja. Si multiplicamos a+bj por c+dj, se puede demostrar fácilmente
que el resultado es (ac-bd) para la parte real y (ad+bc) para la imaginaria.

a = 1 + 3j

b = 4 + 1j

print(a*b) #(1+13j)

División de complejos

También podemos hacer la división. Si quieres saber cómo se dividen dos números complejos y su
demostración matemática, te dejamos este enlace.

a = 1 + 3j

b = 4 + 1j

print(a/b) #(0.41+0.64j)

Conjugado de complejos

Por último, podemos realizar el conjugado de un número complejo en Python con el método conjugate().
Calcular el conjugado consiste en negar la parte imaginaria, es decir, cambiar si signo de + a - y
viceversa.

a = 1 + 1j

print(a.conjugate()) #(1-1j)

Librería cmath

Si quieres realizar más operaciones con número complejos, tal vez quieras echar un vistazo a la
librería cmath, que es mucho más completa, y compleja.
Algunas de las cosas que puedes hacer son las siguientes:

• Calcular la fase, que es el ángulo que forma el vector con el eje x, en radianes.

• Calcular la forma polar, es decir módulo y ángulo.

import cmath

print(cmath.phase(complex(5, 0))) # 0.0

print(cmath.polar(complex(5, 5))) # (7.0710678118654755, 0.7853981633974483)

Cadenas Python

Las cadenas en Python o strings son un tipo inmutable que permite almacenar secuencias de
caracteres. Para crear una, es necesario incluir el texto entre comillas dobles ". Puedes obtener más
ayuda con el comando help(str).

s = "Esto es una cadena"

print(s) #Esto es una cadena

print(type(s)) #<class 'str'>

También es valido declarar las cadenas con comillas simples simples '.

s = 'Esto es otra cadena'

print(s) #Esto es otra cadena

print(type(s)) #<class 'str'>

Las cadenas no están limitadas en tamaño, por lo que el único límite es la memoria de tu ordenador. Una
cadena puede estar también vacía.

s = ''

Una situación que muchas veces se puede dar, es cuando queremos introducir una comilla, bien sea
simple ' o doble " dentro de una cadena. Si lo hacemos de la siguiente forma tendríamos un error, ya que
Python no sabe muy bien donde empieza y termina.

#s = "Esto es una comilla doble " de ejemplo" # Error!

Para resolver este problema debemos recurrir a las secuencias de escape. En Python hay varias, pero las
analizaremos con más detalle en otro capítulo. Por ahora, la más importante es \", que nos permite
incrustar comillas dentro de una cadena.

s = "Esto es una comilla doble \" de ejemplo"

print(s) #Esto es una comilla doble " de ejemplo


También podemos incluir un salto de línea dentro de una cadena, lo que significa que lo que esté
después del salto, se imprimirá en una nueva línea.

s = "Primer linea\nSegunda linea"

print(s)

#Primer linea

#Segunda linea

También podemos usar \ acompañado de un número, lo que imprimirá el carácter asociado. En este
caso imprimimos el carácter 110 que se corresponde con la H.

print("\110\110") #HH

Para saber más: Te recomendamos que busques información sobre ASCI y Unicode. Ambos son
conceptos muy útiles a la hora de entender los strings.

Se puede definir una cadena que ocupe varias líneas usando triple """ comilla. Puede ser muy útil si
tenemos textos muy largo que no queremos tener en una sola línea.

Existe también otra forma de declarar cadenas llamado raw strings. Usando como prefijo r, la cadena
ignora todos las secuencias de escape, por lo que la salida es diferente a la anterior.

print(r"\110\110") #\110\110

print("""La siguiente

cadena ocupa

varias lineas""")

Formateo de cadenas

Tal vez queramos declarar una cadena que contenga variables en su interior, como números o incluso
otras cadenas. Una forma de hacerlo sería concatenando la cadena que queremos con otra usando el
operador +. Nótese que str() convierte en string lo que se pasa como parámetro.

x=5

s = "El número es: " + str(x)

print(s) #El número es: 5

Otra forma es usando %. Por un lado tenemos %s que indica el tipo que se quiere imprimir, y por otro a la
derecha del % tenemos la variable a imprimir. Para imprimir una cadena se usaría %s o %f para un valor
en coma flotante.

Para saber más: En el siguiente enlace puedes encontrar más información sobre el uso de %.

x=5

s = "El número es: %d" % x


print(s) #El número es: 5

Si tenemos más de una variable, también se puede hacer pasando los parámetros dentro de (). Si vienes
de lenguajes como C, esta forma te resultará muy familiar. No obstante, esta no es la forma preferida de
hacerlo, ahora que tenemos nuevas versiones de Python.

s = "Los números son %d y %d." % (5, 10)

print(s) #Los números son 5 y 10.

Una forma un poco más moderna de realizar lo mismo, es haciendo uso de format().

s = "Los números son {} y {}".format(5, 10)

print(s) #Los números son 5 y 10

Es posible también darle nombre a cada elemento, y format() se encargará de reemplazar todo.

s = "Los números son {a} y {b}".format(a=5, b=10)

print(s) #Los números son 5 y 10

Por si no fueran pocas ya, existe una tercera forma de hacerlo introducida en la versión 3.6 de Python.
Reciben el nombre de cadenas literales o f-strings. Esta nueva característica, permite incrustar
expresiones dentro de cadenas.

a = 5; b = 10

s = f"Los números son {a} y {b}"

print(s) #Los números son 5 y 10

Puedes incluso hacer operaciones dentro de la creación del string.

a = 5; b = 10

s = f"a + b = {a+b}"

print(s) #a + b = 15

Puedes incluso llamar a una función dentro.

def funcion():

return 20

s = f"El resultado de la función es {funcion()}"

print(s) #El resultado de la funcion es 20

Ejemplos string

Para entender mejor la clase string, vamos a ver unos ejemplos de como se comportan. Podemos sumar
dos strings con el operador +.
s1 = "Parte 1"

s2 = "Parte 2"

print(s1 + " " + s2) #Parte 1 Parte 2

Se puede multiplicar un string por un int. Su resultado es replicarlo tantas veces como el valor del
entero.

s = "Hola "

print(s*3) #Hola Hola Hola

Podemos ver si una cadena esta contenida en otra con in.

print("mola" in "Python mola") #True

Con chr() and ord() podemos convertir entre carácter y su valor numérico que lo representa y viceversa.
El segundo sólo función con caracteres, es decir, un string con un solo elemento.

print(chr(8364)) #€

print(ord("€")) #110

La longitud de una cadena viene determinada por su número de caracteres, y se puede consultar con la
función len().

print(len("Esta es mi cadena"))

Como hemos visto al principio, se puede convertir a string otras clases, como int o float.

x = str(10.4)

print(x) #10.4

print(type(x)) #<class 'str'>

También se pueden indexar las cadenas, como si de una lista se tratase.

x = "abcde"

print(x[0]) #a

print(x[-1]) #e

Del mismo modo, se pueden crear cadenas más pequeñas partiendo de una grande, usando indicando
el primer elemento y el último que queremos tomar menos uno.

x = "abcde"

print(x[0:2])

Si no se indica ningún valor a la derecha de los : se llega hasta el final.

x = "abcde"
print(x[2:])

Es posible también crear subcadenas que contengan elementos salteados y no contiguos añadiendo un
tercer elemento entre []. Indica los elementos que se saltan. En el siguiente ejemplo se toman elementos
del 0 al 5 de dos en dos.

x = "abcde"

print(x[0:5:2]) #ace

Tampoco es necesario saber el tamaño de la cadena, y el segundo valor se podría omitir. El siguiente
ejemplo es igual al anterior.

x = "abcde"

print(x[0::2]) #ace

Métodos string

Algunos de los métodos de la clase string.

capitalize()

El método capitalize() se aplica sobre una cadena y la devuelve con su primera letra en mayúscula.

s = "mi cadena"

print(s.capitalize()) #Mi cadena

lower()

El método lower() convierte todos los caracteres alfabéticos en minúscula.

s = "MI CADENA"

print(s.lower()) #mi cadena

swapcase()

El método swapcase() convierte los caracteres alfabéticos con mayúsculas en minúsculas y viceversa.

s = "mI cAdEnA"

print(s.swapcase()) #Mi CaDeNa

upper()

El método upper() convierte todos los caracteres alfabéticos en mayúsculas.

s = "mi cadena"

print(s.upper())

count(<sub>[, <start>[, <end>]])


El método count() permite contar las veces que otra cadena se encuentra dentro de la primera. Permite
también dos parámetros opcionales que indican donde empezar y acabar de buscar.

s = "el bello cuello "

print(s.count("llo")) #2

isalnum()

El método isalnum() devuelve True si la cadena esta formada únicamente por caracteres
alfanuméricos, False de lo contrario. Caracteres como @ o & no son alfanumericos.

s = "correo@dominio.com"

print(s.isalnum())

isalpha()

El método isalpha() devuelve True si todos los caracteres son alfabéticos, False de lo contrario.

s = "abcdefg"

print(s.isalpha())

strip([<chars>])

El método strip() elimina a la izquierda y derecha el carácter que se le introduce. Si se llama sin
parámetros elimina los espacios. Muy útil para limpiar cadenas.

s = " abc "

print(s.strip()) #abc

zfill(<width>)

El método zfill() rellena la cadena con ceros a la izquierda hasta llegar a la longitud pasada como
parámetro.

s = "123"

print(s.zfill(5)) #00123

join(<iterable>)

El método join() devuelve la primera cadena unida a cada uno de los elementos de la lista que se le pasa
como parámetro.

s = " y ".join(["1", "2", "3"])

print(s) #1 y 2 y 3

split(sep=None, maxsplit=-1)
El método split() divide una cadena en subcadenas y las devuelve almacenadas en una lista. La división
es realizada de acuerdo a el primer parámetro, y el segundo parámetro indica el número máximo de
divisiones a realizar.

s = "Python,Java,C"

print(s.split(",")) #['Python', 'Java', 'C']

Listas en Python

Las listas en Python son un tipo de dato que permite almacenar datos de cualquier tipo. Son mutables y
dinámicas, lo cual es la principal diferencia con los sets y las tuplas.

Crear listas Python

Las listas en Python son uno de los tipos o estructuras de datos más versátiles del lenguaje, ya que
permiten almacenar un conjunto arbitrario de datos. Es decir, podemos guardar en ellas prácticamente
lo que sea. Si vienes de otros lenguajes de programación, se podría decir que son similares a los arrays.

lista = [1, 2, 3, 4]

También se puede crear usando list y pasando un objeto iterable.

lista = list("1234")

Una lista sea crea con [] separando sus elementos con comas ,. Una gran ventaja es que pueden
almacenar tipos de datos distintos.

lista = [1, "Hola", 3.67, [1, 2, 3]]

Algunas propiedades de las listas:

• Son ordenadas, mantienen el orden en el que han sido definidas

• Pueden ser formadas por tipos arbitrarios

• Pueden ser indexadas con [i].

• Se pueden anidar, es decir, meter una dentro de la otra.

• Son mutables, ya que sus elementos pueden ser modificados.

• Son dinámicas, ya que se pueden añadir o eliminar elementos.

Acceder y modificar listas

Si tenemos una lista a con 3 elementos almacenados en ella, podemos acceder a los mismos usando
corchetes y un índice, que va desde 0 a n-1 siendo n el tamaño de la lista.

a = [90, "Python", 3.87]

print(a[0]) #90

print(a[1]) #Python
print(a[2]) #3.87

Se puede también acceder al último elemento usando el índice [-1].

a = [90, "Python", 3.87]

print(a[-1]) #3.87

De la misma manera, al igual que [-1] es el último elemento, podemos acceder a [-2] que será el
penúltimo.

print(a[-2]) #Python

Y si queremos modificar un elemento de la lista, basta con asignar con el operador = el nuevo valor.

a[2] = 1

print(a) #[90, 'Python', 1]

Un elemento puede ser eliminado con diferentes métodos como veremos a continuación, o con del y la
lista con el índice a eliminar.

l = [1, 2, 3, 4, 5]

del l[1]

print(l) #[1, 3, 4, 5]

También podemos tener listas anidadas, es decir, una lista dentro de otra. Incluso podemos tener una
lista dentro de otra lista y a su vez dentro de otra lista. Para acceder a sus elementos sólo tenemos que
usar [] tantas veces como niveles de anidado tengamos.

x = [1, 2, 3, ['p', 'q', [5, 6, 7]]]

print(x[3][0]) #p

print(x[3][2][0]) #5

print(x[3][2][2]) #7

También es posible crear sublistas más pequeñas de una más grande. Para ello debemos de usar : entre
corchetes, indicando a la izquierda el valor de inicio, y a la izquierda el valor final que no está incluido.
Por lo tanto [0:2] creará una lista con los elementos [0] y [1] de la original.

l = [1, 2, 3, 4, 5, 6]

print(l[0:2]) #[1, 2]

print(l[2:6]) #[3, 4, 5, 6]

Y de la misma manera podemos modificar múltiples valores de la lista a la vez usando :.

l = [1, 2, 3, 4, 5, 6]

l[0:3] = [0, 0, 0]
print(l) #[0, 0, 0, 4, 5, 6]

Hay ciertos operadores como el + que pueden ser usados sobre las listas.

l = [1, 2, 3]

l += [4, 5]

print(l) #[1, 2, 3, 4, 5]

Y una funcionalidad muy interesante es que se puede asignar una lista con n elementos a n variables.

l = [1, 2, 3]

x, y, z = l

print(x, y, z) #1 2 3

Iterar listas

En Python es muy fácil iterar una lista, mucho más que en otros lenguajes de programación.

lista = [5, 9, 10]

for l in lista:

print(l)

#5

#9

#10

Si necesitamos un índice acompañado con la lista, que tome valores desde 0 hasta n-1, se puede hacer
de la siguiente manera.

lista = [5, 9, 10]

for index, l in enumerate(lista):

print(index, l)

#0 5

#1 9

#2 10

O si tenemos dos listas y las queremos iterar a la vez, también es posible hacerlo.

lista1 = [5, 9, 10]

lista2 = ["Jazz", "Rock", "Djent"]

for l1, l2 in zip(lista1, lista2):


print(l1, l2)

#5 Jazz

#9 Rock

#10 Djent

Y por supuesto, también se pueden iterar las listas usando los índices como hemos visto al principio, y
haciendo uso de len(), que nos devuelve la longitud de la lista.

lista1 = [5, 9, 10]

for i in range(0, len(lista)):

print(lista1[i])

#5

#9

#10

Métodos listas

append(<obj>)

El método append() añade un elemento al final de la lista.

l = [1, 2]

l.append(3)

print(l) #[1, 2, 3]

extend(<iterable>)

El método extend() permite añadir una lista a la lista inicial.

l = [1, 2]

l.extend([3, 4])

print(l) #[1, 2, 3, 4]

insert(<index>, <obj>)

El método insert() añade un elemento en una posición o índice determinado.

l = [1, 3]

l.insert(1, 2)

print(l) #[1, 2, 3]

remove(<obj>)
El método remove() recibe como argumento un objeto y lo borra de la lista.

l = [1, 2, 3]

l.remove(3)

print(l) #[1, 2]

pop(index=-1)

El método pop() elimina por defecto el último elemento de la lista, pero si se pasa como parámetro
un índice permite borrar elementos diferentes al último.

l = [1, 2, 3]

l.pop()

print(l) #[1, 2]

reverse()

El método reverse() inverte el órden de la lista.

l = [1, 2, 3]

l.reverse()

print(l) #[3, 2, 1]

sort()

El método sort() ordena los elementos de menos a mayor por defecto.

l = [3, 1, 2]

l.sort()

print(l) #[1, 2, 3]

Y también permite ordenar de mayor a menor si se pasa como parámetro reverse=True.

l = [3, 1, 2]

l.sort(reverse=True)

print(l) #[3, 2, 1]

index(<obj>[,index])

El método index() recibe como parámetro un objeto y devuelve el índice de su primera aparición. Como
hemos visto en otras ocasiones, el índice del primer elemento es el 0.

l = ["Periphery", "Intervals", "Monuments"]

print(l.index("Intervals"))
También permite introducir un parámetro opcional que representa el índice desde el que comenzar la
búsqueda del objeto. Es como si ignorara todo lo que hay antes de ese índice para la búsqueda, en este
caso el 4.

l = [1, 1, 1, 1, 2, 1, 4, 5]

print(l.index(1, 4)) #5

Set

Los sets en Python son una estructura de datos usada para almacenar elementos de una manera similar
a las listas, pero con ciertas diferencias.

Crear set Python

Los set en Python son un tipo que permite almacenar varios elementos y acceder a ellos de una forma
muy similar a las listas pero con ciertas diferencias:

• Los elementos de un set son único, lo que significa que no puede haber elementos duplicados.

• Los set son desordenados, lo que significa que no mantienen el orden de cuando son
declarados.

• Sus elementos deben ser inmutables.

Para entender mejor los sets, es necesario entender ciertos conceptos matemáticos como la teoría de
conjuntos.

Para crear un set en Python se puede hacer con set() y pasando como entrada cualquier tipo iterable,
como puede ser una lista. Se puede ver como a pesar de pasar elementos duplicados como dos 8 y en
un orden determinado, al imprimir el set no conserva ese orden y los duplicados se han eliminado.

s = set([5, 4, 6, 8, 8, 1])

print(s) #{1, 4, 5, 6, 8}

print(type(s)) #<class 'set'>

Se puede hacer lo mismo haciendo uso de {} y sin usar la palabra set() como se muestra a continuación.

s = {5, 4, 6, 8, 8, 1}

print(s) #{1, 4, 5, 6, 8}

print(type(s)) #<class 'set'>

Operaciones con sets

A diferencia de las listas, en los set no podemos modificar un elemento a través de su índice. Si lo
intentamos, tendremos un TypeError.

s = set([5, 6, 7, 8])

#s[0] = 3 #Error! TypeError


Los elementos de un set deben ser inmutables, por lo que un elemento de un set no puede ser ni un
diccionario ni una lista. Si lo intentamos tendremos un TypeError

lista = ["Perú", "Argentina"]

#s = set(["México", "España", lista]) #Error! TypeError

Los sets se pueden iterar de la misma forma que las listas.

s = set([5, 6, 7, 8])

for ss in s:

print(ss) #8, 5, 6, 7

Con la función len() podemos saber la longitud total del set. Como ya hemos indicado, los duplicados
son eliminados.

s = set([1, 2, 2, 3, 4])

print(len(s)) #4

También podemos saber si un elemento está presente en un set con el operador in. Se el valor existe en
el set, se devolverá True.

s = set(["Guitarra", "Bajo"])

print("Guitarra" in s) #True

Los sets tienen además diferentes funcionalidades, que se pueden aplicar en forma de operador o de
método. Por ejemplo, el operador | nos permite realizar la unión de dos sets, lo que equivale a juntarlos.
El equivalente es el método union() que vemos a continuación.

s1 = set([1, 2, 3])

s2 = set([3, 4, 5])

print(s1 | s2) #{1, 2, 3, 4, 5}

Métodos sets

s.add(<elem>)

El método add() permite añadir un elemento al set.

l = set([1, 2])

l.add(3)

print(l) #{1, 2, 3}

s.remove(<elem>)

El método remove() elimina el elemento que se pasa como parámetro. Si no se encuentra, se lanza la
excepción KeyError.
s = set([1, 2])

s.remove(2)

print(s) #{1}

s.discard(<elem>)

El método discard() es muy parecido al remove(), borra el elemento que se pasa como parámetro, y si no
se encuentra no hace nada.

s = set([1, 2])

s.discard(3)

print(s) #{1, 2}

s.pop()

El método pop() elimina un elemento aleatorio del set.

s = set([1, 2])

s.pop()

print(s) #{2}

s.clear()

El método clear() elimina todos los elementos de set.

s = set([1, 2])

s.clear()

print(s) #set()

Otros

Los sets cuentan con una gran cantidad de métodos que permiten realizar operaciones con dos o más,
como la unión o la intersección.

Podemos calcular la unión entre dos sets usando el método union(). Esta operación representa la
“mezcla” de ambos sets. Nótese que el método puede ser llamado con más parámetros de entrada, y su
resultado será la unión de todos los sets.

s1 = {1, 2, 3}

s2 = {3, 4, 5}

print(s1.union(s2)) #{1, 2, 3, 4, 5}

También podemos calcular la intersección entre dos o más set. Su resultado serán aquellos elementos
que pertenecen a ambos sets.
s1 = {1, 2, 3}

s2 = {3, 4, 5}

print(s1.intersection(s2)) #{3}

Los set en Python tiene gran cantidad de métodos, por lo que lo dejaremos para otro capítulo, pero aquí
os dejamos con un listado de ellos:

• s1.union(s2[, s3 ...])

• s1.intersection(s2[, s3 ...])

• s1.difference(s2[, s3 ...])

• s1.symmetric_difference(s2)

• s1.isdisjoint(s2)

• s1.issubset(s2)

• s1.issuperset(s2)

• s1.update(s2[, s3 ...])

• s1.intersection_update(s2[, s3 ...])

• s1.difference_update(s2[, s3 ...])

• s1.symmetric_difference_update(s2)

Tupla (tuple)
Las tuplas en Python son un tipo o estructura de datos que permite almacenar datos de una manera
muy parecida a las listas, con la salvedad de que son inmutables.

Crear tupla Python


Las tuplas en Python o tuples son muy similares a las listas, pero con dos diferencias.
Son inmutables, lo que significa que no pueden ser modificadas una vez declaradas, y en vez de
inicializarse con corchetes se hace con () . Dependiendo de lo que queramos hacer, las tuplas
pueden ser más rápidas.
tupla = (1, 2, 3)
print(tupla) #(1, 2, 3)

También pueden declararse sin () , separando por , todos sus elementos.


tupla = 1, 2, 3
print(type(tupla)) #<class 'tuple'>
print(tupla) #(1, 2, 3)
Operaciones con tuplas
Como hemos comentado, las tuplas son tipos inmutables, lo que significa que una vez asignado su
valor, no puede ser modificado. Si se intenta, tendremos un TypeError .
tupla = (1, 2, 3)
#tupla[0] = 5 # Error! TypeError

Al igual que las listas, las tuplas también pueden ser anidadas.
tupla = 1, 2, ('a', 'b'), 3
print(tupla) #(1, 2, ('a', 'b'), 3)
print(tupla[2][0]) #a

Y también es posible convertir una lista en tupla haciendo uso de al función tuple() .

lista = [1, 2, 3]
tupla = tuple(lista)
print(type(tupla)) #<class 'tuple'>
print(tupla) #(1, 2, 3)

Se puede iterar una tupla de la misma forma que se hacía con las listas.
tupla = [1, 2, 3]
for t in tupla:
print(t) #1, 2, 3

Y se puede también asignar el valor de una tupla con n elementos a n variables.


l = (1, 2, 3)
x, y, z = l
print(x, y, z) #1 2 3

Aunque tal vez no tenga mucho sentido a nivel práctico, es posible crear una tupla de un solo
elemento. Para ello debes usar , antes del paréntesis, porque de lo contrario (2) sería
interpretado como int .
tupla = (2,)
print(type(tupla)) #<class 'tuple'>

Métodos tuplas

count(<obj>)

El método count() cuenta el número de veces que el objeto pasado como parámetro se ha
encontrado en la lista.
l = [1, 1, 1, 3, 5]
print(l.count(1)) #3
index(<obj>[,index])

El método index() busca el objeto que se le pasa como parámetro y devuelve el índice en el que se
ha encontrado.
l = [7, 7, 7, 3, 5]
print(l.index(5)) #4

En el caso de no encontrarse, se devuelve un ValueError .

l = [7, 7, 7, 3, 5]
#print(l.index(35)) #Error! ValueError

El método index() también acepta un segundo parámetro opcional, que indica a partir de que
índice empezar a buscar el objeto.
l = [7, 7, 7, 3, 5]
print(l.index(7, 2)) #2
Diccionario

Los diccionarios en Python son una estructura de datos que permite almacenar su contenido en forma
de llave y valor.

Crear diccionario Python

Un diccionario en Python es una colección de elementos, donde cada uno tiene una llave key y un
valor value. Los diccionarios se pueden crear con paréntesis {} separando con una coma cada par key:
value. En el siguiente ejemplo tenemos tres keys que son el nombre, la edad y el documento.

d1 = {

"Nombre": "Sara",

"Edad": 27,

"Documento": 1003882

print(d1)

#{'Nombre': 'Sara', 'Edad': 27, 'Documento': 1003882}

Otra forma equivalente de crear un diccionario en Python es usando dict() e introduciendo los pares key:
value entre paréntesis.

d2 = dict([

('Nombre', 'Sara'),

('Edad', 27),

('Documento', 1003882),
])

print(d2)

#{'Nombre': 'Sara', 'Edad': '27', 'Documento': '1003882'}

También es posible usar el constructor dict() para crear un diccionario.

d3 = dict(Nombre='Sara',

Edad=27,

Documento=1003882)

print(d3)

#{'Nombre': 'Sara', 'Edad': 27, 'Documento': 1003882}

Algunas propiedades de los diccionario en Python son las siguientes:

• Son dinámicos, pueden crecer o decrecer, se pueden añadir o eliminar elementos.

• Son indexados, los elementos del diccionario son accesibles a través del key.

• Y son anidados, un diccionario puede contener a otro diccionario en su campo value.

Acceder y modificar elementos

Se puede acceder a sus elementos con [] o también con la función get()

print(d1['Nombre']) #Sara

print(d1.get('Nombre')) #Sara

Para modificar un elemento basta con usar [] con el nombre del key y asignar el valor que queremos.

d1['Nombre'] = "Laura"

print(d1)

#{'Nombre': Laura', 'Edad': 27, 'Documento': 1003882}

Si el key al que accedemos no existe, se añade automáticamente.

d1['Direccion'] = "Calle 123"

print(d1)

#{'Nombre': 'Laura', 'Edad': 27, 'Documento': 1003882, 'Direccion': 'Calle 123'}

Iterar diccionario

Los diccionarios se pueden iterar de manera muy similar a las listas u otras estructuras de datos. Para
imprimir los key.

# Imprime los key del diccionario


for x in d1:

print(x)

#Nombre

#Edad

#Documento

#Direccion

Se puede imprimir también solo el value.

# Impre los value del diccionario

for x in d1:

print(d1[x])

#Laura

#27

#1003882

#Calle 123

O si queremos imprimir el key y el value a la vez.

# Imprime los key y value del diccionario

for x, y in d1.items():

print(x, y)

#Nombre Laura

#Edad 27

#Documento 1003882

#Direccion Calle 123

Diccionarios anidados

Los diccionarios en Python pueden contener uno dentro de otro. Podemos ver como los valores anidado
uno y dos del diccionario d contienen a su vez otro diccionario.

anidado1 = {"a": 1, "b": 2}

anidado2 = {"a": 1, "b": 2}

d={

"anidado1" : anidado1,
"anidado2" : anidado2

print(d)

#{'anidado1': {'a': 1, 'b': 2}, 'anidado2': {'a': 1, 'b': 2}}

Métodos diccionarios Python

clear()

El método clear() elimina todo el contenido del diccionario.

d = {'a': 1, 'b': 2}

d.clear()

print(d) #{}

get(<key>[,<default>])

El método get() nos permite consultar el value para un key determinado. El segundo parámetro es
opcional, y en el caso de proporcionarlo es el valor a devolver si no se encuentra la key.

d = {'a': 1, 'b': 2}

print(d.get('a')) #1

print(d.get('z', 'No encontrado')) #No encontrado

items()

El método items() devuelve una lista con los keys y values del diccionario. Si se convierte en list se puede
indexar como si de una lista normal se tratase, siendo los primeros elementos las key y los segundos
los value.

d = {'a': 1, 'b': 2}

it = d.items()

print(it) #dict_items([('a', 1), ('b', 2)])

print(list(it)) #[('a', 1), ('b', 2)]

print(list(it)[0][0]) #a

keys()

El método keys() devuelve una lista con todas las keys del diccionario.

d = {'a': 1, 'b': 2}

k = d.keys()

print(k) #dict_keys(['a', 'b'])


print(list(k)) #['a', 'b']

values()

El método values() devuelve una lista con todos los values o valores del diccionario.

d = {'a': 1, 'b': 2}

print(list(d.values())) #[1, 2]

pop(<key>[,<default>])

El método pop() busca y elimina la key que se pasa como parámetro y devuelve su valor asociado. Daría
un error si se intenta eliminar una key que no existe.

d = {'a': 1, 'b': 2}

d.pop('a')

print(d) #{'b': 2}

También se puede pasar un segundo parámetro que es el valor a devolver si la key no se ha encontrado.
En este caso si no se encuentra no habría error.

d = {'a': 1, 'b': 2}

d.pop('c', -1)

print(d) #{'a': 1, 'b': 2}

popitem()

El método popitem() elimina de manera aleatoria un elemento del diccionario.

d = {'a': 1, 'b': 2}

d.popitem()

print(d)

#{'a': 1}

update(<obj>)

El método update() se llama sobre un diccionario y tiene como entrada otro diccionario. Los value son
actualizados y si alguna key del nuevo diccionario no esta, es añadida.

d1 = {'a': 1, 'b': 2}

d2 = {'a': 0, 'd': 400}

d1.update(d2)

print(d1)

#{'a': 0, 'b': 2, 'd': 400}


Frozenset

Los frozenset en Python son una estructura de datos muy similar a los set, con la salvedad de que
son inmutables, es decir, no pueden ser modificados una vez declarados.

Crear frozenset Python

Los frozensets en Python son un tipo muy parecido a los sets con la salvedad de que son inmutables, es
decir, están congelados y no pueden ser modificados una vez inicializados.

fs = frozenset([1, 2, 3])

print(fs) #frozenset({1, 2, 3})

print(type(fs)) #<class 'frozenset'>

Ejemplos frozenset

Dado que son inmutables, cualquier intento de modificación con los métodos que ya hemos visto en
otros capítulos como add() o clear() dará un error, ya que los frozenset no disponen de esos métodos.

fs = frozenset([1, 2, 3])

#fs.add(4) #Error! AttributeError

#fs.clear() #Error! AttributeError

Los frozenset pueden ser útiles cuando queremos usar un set pero se requiere que el tipo sea inmutable.
Algo no muy común, pero que podría darse, es crear un set de sets. Es decir, un ser que contiene como
elementos a otros sets. El siguiente código daría un TypeError ya que los elementos de un set deben ser
por definición inmutables.

s1 = {1, 2}

s2 = {3, 4}

#s3 = {s1, s2} # Error! TypeError

Para resolver este problema, podemos crear un set de frozensets. Esto si es posible ya que
el frozenset es un tipo inmutable.

s1 = frozenset([1, 2])

s2 = frozenset([3, 4])

s3 = {s1, s2}

print(s3) #{frozenset({3, 4}), frozenset({1, 2})}

Lo mismo aplica a los diccionarios, ya que su key debe ser un tipo inmutable. Si intentamos crear un
diccionario con set como key, obtendremos un TypeError.

s1 = set([1, 2])
s2 = set([3, 4])

#d = {s1: "Texto1", s2: "Texto2"} # Error! TypeError

Pero si podemos crear un diccionario donde sus key son frozenset, ya que son un tipo inmutable.

s1 = frozenset([1, 2])

s2 = frozenset([3, 4])

d = {s1: "Texto1", s2: "Texto2"}

print(d) #{frozenset({1, 2}): 'Texto1', frozenset({3, 4}): 'Texto2'}

Tal vez te interese leer acerca de otras estructuras de datos similares como los sets o las listas.

Cast en Python

Hacer un cast o casting significa convertir un tipo de dato a otro. Anteriormente hemos visto tipos como
los int, string o float. Pues bien, es posible convertir de un tipo a otro.

Pero antes de nada, veamos los diferentes tipos de cast o conversión de tipos que se pueden hacer.
Existen dos:

• Conversión implícita: Es realizada automáticamente por Python. Sucede cuando realizamos


ciertas operaciones con dos tipos distintos.

• Conversión explícita: Es realizada expresamente por nosotros, como por ejemplo convertir
de str a int con str().

Conversión implícita

Esta conversión de tipos es realizada automáticamente por Python, prácticamente sin que nos demos
cuenta. Aún así, es importante saber lo que pasa por debajo para evitar problemas futuros.

El ejemplo más sencillo donde podemos ver este comportamiento es el siguiente:

a = 1 # <class 'int'>

b = 2.3 # <class 'float'>

a=a+b

print(a) # 3.3

print(type(a)) # <class 'float'>

• a es un int

• b es un float

Pero si sumamos a y b y almacenamos el resultado en a, podemos ver como internamente Python ha


convertido el int en float para poder realizar la operación, y la variable resultante es float
Sin embargo hay otros casos donde Python no es tan listo y no es capaz de realizar la conversión. Si
intentamos sumar un int a un string, tendremos un error TypeError.

a=1

b = "2.3"

c=a+b

# TypeError: unsupported operand type(s) for +: 'int' and 'str'

Si te das cuenta, es lógico que esto sea así, ya que en este caso b era "2.3", pero ¿y si fuera "Hola"?
¿Cómo se podría sumar eso? No tiene sentido.

Conversión explicita

Por otro lado, podemos hacer conversiones entre tipos o cast de manera explícita haciendo uso de
diferentes funciones que nos proporciona Python. Las más usadas son las siguientes:

• float(), str(), int(), list(), set()

• Y algunas otras como hex(), oct() y bin()

Convertir float a int

Para convertir de float a int debemos usar float(). Pero mucho cuidado, ya que el tipo entero no puede
almacena decimales, por lo que perderemos lo que haya después de la coma.

a = 3.5

a = int(a)

print(a)

# Salida: 3

Convertir float a string

Podemos convertir un float a string con str(). Podemos ver en el siguiente código como cambia el tipo
de a después de hacer el cast.

a = 3.5

print(type(a)) # <class 'float'>

a = str(a)

print(type(a)) # <class 'str'>

Convertir string a float


Podemos convertir un string a float usando float(). Es importante notar que se usa el . como separador.

a = "35.5"

print(float(a))

# Salida: 35.5

El siguiente código daría un error, dado que , no se reconoce por defecto como separador decimal.

a = "35,5"

print(float(a))

# Salida: ValueError: could not convert string to float: '35,5'

Y por último, resulta obvio pensar que el siguiente código dará un error también.

a = "Python"

print(float(a))

# Salida: ValueError: could not convert string to float: 'Python'

Convertir string a int

Al igual que la conversión a float del caso anterior, podemos convertir de string a int usando int().

a = "3"

print(type(a)) # <class 'str'>

a = int(a)

print(type(a)) # <class 'int'>

Cuidado ya que no es posible convertir a int cualquier valor.

a = "Python"

a = int(a)

# ValueError: invalid literal for int() with base 10: 'Python'

Convertir int a string

La conversión de int a string se puede realizar con str().

A diferencia de otras conversiones, esta puede hacerse siempre, ya que cualquier valor entero que se
nos ocurra poner en a, podrá ser convertido a string.

a = 10

print(type(a)) # <class 'int'>

a = str(a)
print(type(a)) # <class 'str'>

Convertir a list

Es también posible hacer un cast a lista, desde por ejemplo un set. Para ello podemos usar list().

a = {1, 2, 3}

b = list(a)

print(type(a)) # <class 'set'>

print(type(b)) # <class 'list'>

Convertir a set

Y de manera completamente análoga, podemos convertir de lista a set haciendo uso de set().

a = ["Python", "Mola"]

b = set(a)

print(type(a)) # <class 'list'>

print(type(b)) # <class 'set'>

Esperamos que esto haya servido para aclarar el concepto de casts o conversiones de tipos en Python.
Es muy importante tener bien claro el tipo de datos con el que estamos trabajando en cada momento, ya
que Python es un lenguaje con tipado dinámico, algo que puede ser una gran ventaja, pero también
causa de muchos quebraderos de cabeza.

Y recuerda, la función type() es tu amiga.

Si quieres saber más, tal vez te interese el concepto de duck typing.

Colecciones

Colecciones

Una colección permite agrupar varios objetos bajo un mismo nombre. Por ejemplo, si necesitamos
almacenar en nuestro programa los nombres de los alumnos de un curso de programación, será más
conveniente ubicarlos a todos dentro de una misma colección de nombre alumnos, en lugar de crear los
objetos alumno1, alumno2, etc.

En Python existen tres colecciones básicas, a saber: las listas, las tuplas y los diccionarios.

Listas

Una lista es un conjunto ordenado de objetos. Por objetos entendemos cualquiera de los tipos de dato ya
mencionados, incluso otras listas.
Para crear una lista, especificamos sus elementos entre corchetes y separados por comas.

>>> lenguajes = ["Python", "Java", "C", "C++"]

Dado que se trata de una colección ordenada, accedemos a cada uno de los elementos a partir de su
posición, comenzando desde el 0.

>>> lenguajes[0]

'Python'

>>> lenguajes[1]

'Java'

(En Python, las cadenas de caracteres pueden estar formadas tanto por comillas dobles como por
comillas simples).

La posición de un elemento, también conocida como índice, aumenta de izquierda a derecha cuando
éste es positivo. Indicando un número negativo, los elementos se obtienen de derecha a izquierda.

>>> lenguajes[-1]

'C++'

>>> lenguajes[-2]

'C'

Para cambiar un elemento, combinamos la nomenclatura descrita con la asignación.

>>> lenguajes[1] = "Rust"

>>> lenguajes

['Python', 'Rust', 'C', 'C++']

Para remover un elemento, utilizamos la palabra reservada del.

>>> del lenguajes[1]

>>> lenguajes

['Python', 'C', 'C++']

Nótese que cuando un elemento es eliminado, a excepción del último, todos los que lo suceden se
corren una posición hacia la izquierda.

La operación inversa es la de insertar un elemento en una posición determinada, desplazando el resto


que le sucede una posición hacia la derecha, vía el método (volveremos sobre este concepto más
adelante) insert().

# Insertar la cadena "Elixir" en la posición 1.

>>> lenguajes.insert(1, "Elixir")


>>> lenguajes

['Python', 'Elixir', 'C', 'C++']

Por último, la operación más común es la de agregar un elemento al final de la lista. Para ello
empleamos append().

>>> lenguajes.append("Haskell")

>>> lenguajes

['Python', 'Elixir', 'C', 'C++', 'Haskell']

¡Genial! Baste por el momento con lo que se ha dicho sobre las listas. Conoceremos más propiedades
en la sección de Slicing.

Identidad, tipo y valor

Python es un lenguaje de programación orientado a objetos, y como tal, trata a todos los tipos de datos
como objetos. Un simple entero es un objeto.

# x es un objeto

x=5

Y una función es también un objeto.

# x es un objeto

def x():

pass

Sabido esto, tal vez te preguntes ¿y entonces cuál es la diferencia? ¿en qué se diferencian los objetos?
Pues bien, cada objeto viene identificado por su identidad, tipo y valor:

• Identidad: Nunca cambia e identifica de manera unívoca al objeto. El operador is nos permite
saber si dos objetos son en realidad el mismo. Es decir, si dos variables hacen referencia al
mismo.

• Tipo: Nos indica el tipo al que pertenece, como un float o una lista. La función type() nos indica el
tipo de un determinado objeto. Es la clase a la que pertenece.

• Valor: Todo objeto tiene unas características particulares. Si estas características pueden ser
modificadas, diremos que es un tipo mutable. De lo contrario, que es inmutable.

Veamos un ejemplo con un entero:

• Podemos ver con id(), que se trata de un identificador único. Es importante notar que si
ejecutamos el código diferentes veces, su valor no tiene porqué se el mismo.

• Por otro lado el tipo entero, <class 'int'>.

• Por último tenemos su valor, 10.


x = 10

print("Identidad:", id(x))

print("Tipo:", type(x))

print("Value:", x)

# Identidad: 4474035136

# Tipo: <class 'int'>

# Value: 10

Los enteros en Python, son un tipo inmutable y a continuación explicaremos las implicaciones que tiene
esto.

Mutabilidad

Los diferentes tipos de Python u otros objetos en general, pueden ser clasificados atendiendo a su
mutabilidad. Pueden ser:

• Mutables: Si permiten ser modificados una vez creados.

• Inmutables: Si no permiten ser modificados una vez creados.

Son mutables los siguientes tipos:

• Listas

• Bytearray

• Memoryview

• Diccionarios

• Sets

• Y clases definidas por el usuario

Y son inmutables:

• Booleanos

• Complejos

• Enteros

• Float

• Frozenset

• Cadenas
• Tuplas

• Range

• Bytes

Sabida ya la clasificación, tal vez te preguntes porqué es esto relevante. Pues bien, Python trata de
manera diferente a los tipos mutables e inmutables, y si no entiendes bien este concepto, puedes llegar
a tener comportamientos inesperados en tus programas.

La forma más sencilla de ver la diferencia, es usando las listas en oposición a las tuplas. Las primeras
son mutables, las segundas no.

Una lista l puede ser modificada una vez creada.

# La asignación se puede realizar

l = [1, 2, 3]

l[0] = 0

Como hemos explicado antes, id() nos devuelve un identificador único del objeto. Como puedes
observar, es el mismo antes y después de realizar la modificación.

l = [1, 2, 3]

print(id(l)) #4383854144

l[0] = 0

print(id(l)) #4383854144

Sin embargo, una tupla es inmutable, por lo que la siguiente asignación dará un error.

# La asignación no se puede realizar

t = (1, 2, 3)

t[0] = 0

Aunque la tupla es inmutable, si que habría una forma de modificar el valor de t, pero lo que en realidad
hacemos es crear una nueva tupla y asignarle el mismo nombre.

Se podría hacer algo como lo siguiente, convertir la tupla en lista, modificar la lista y convertir a tupla
otra vez.

t = (1, 2, 3)

print(id(t)) #4483581184

t = list(t)

# Modificar elemento
t[0] = 0

t = tuple(t)

print(t) #(0, 2, 3)

print(id(t)) #4483953088

Como puedes ver, hemos conseguido modificar el valor de t, pero id(t) ya no nos devuelve el mismo
valor. El nombre de la variable es el mismo, pero el objeto al que “apunta” ha cambiado.

Lo mismo pasa con los sets (mutables) y los frozenset (no mutables).

Esto no resulta tan evidente si usamos un entero. Veamos un ejemplo.

Tenemos una variable x con su id y le añadimos una unidad. El resultado es que el identificador cambia,
ya que Python no puede cambiar el 5 al ser el entero un tipo inmutable.

x=5

print(id(x)) # 4525173536

x=x+1

print(id(x)) # 4525173568 (Ha cambiado)

Sin embargo, si usamos una lista, que es un tipo mutable, al intentar modificar la x, dicha modificación
puede ser realizada en la misma variable, por lo que el id() es el mismo

x = [1, 2]

print(id(x)) # 4382432768

x.append(3)

print(id(x)) # 4382432768 (No cambia)

Las principales diferencias entre tipos mutables e inmutables son las siguientes:

• Los tipos inmutables son generalmente más rápidos de acceder. Por lo que si no piensas
modificar una lista, es mejor que uses una tupla.

• Los tipos mutables son perfectos cuando quieres cambiar su contenido repetidas veces.

• Los tipos inmutables son caros de cambiar, ya que lo que se hace en realidad es hacer una copia
de su contenido en un nuevo objeto con las modificaciones.

Excepciones de mutabilidad

Aunque tampoco se trata de una excepción propiamente dicha ya que no rompe con lo explicado
anteriormente, hay casos en los que si podría parecer que se modifica un tipo inmutable.
Imaginemos que tenemos una tupla (inmutable) compuesta por varios elementos, donde hay una lista
(mutable).

l = [4, 5, 6]

# La lista es parte de la tupla

t = (1, 2, 3, l)

print(t) # (1, 2, 3, [4, 5, 6])

# Podemos modificar la lista

l[0] = 0

# Que también modifica la tupla

print(t) # (1, 2, 3, [0, 5, 6])

Aunque parece que hayamos modificado la tupla t, lo que en realidad hemos modificado es la lista l, y
como la tupla referencia a ella, también se ve modificada.

Mutabilidad y paso por valor/referencia

La mutabilidad de los objetos es una característica muy importante cuando tratamos con funciones, ya
que Python los tratará de manera distinta.

Si conoces lenguajes de programación como C, los conceptos de paso por valor o referencia te
resultarán familiares:

• Los tipos inmutables son pasados por valor, por lo tanto dentro de la función se accede a una
copia y no al valor original.

• Los tipos mutables son pasados por referencia, como es el caso de las listas y los diccionarios.
Algo similar a como C pasa las array como punteros.

En otros posts te explicamos con más detalle el paso por valor y referencia, pero veamos un ejemplo.

Tenemos una función que modifica dos variables.

def funcion(a, b):

a = 10

b[0] = 10

# x es un entero
x=5

# y es una lista

y = [5]

Si llamamos a la función con ambas variables, vemos como el valor de x no ha cambiado, pero el de y sí.

funcion(x, y)

print(x) # 5

print(y) # 10

Esto se debe a que a=10 trabaja con un valor de a local a la función, al ser el entero un tipo inmutable.
Sin embargo b[0]=10 actúa sobre la variable original.

Ejercicios y ejemplos

Modificar una tupla con slicing

Como hemos visto anteriormente, realizar la siguiente asignación no es posible.

T = (1, 2, 3, 4, 5)

T[2] = 0

# TypeError: 'tuple' object does not support item assignment

Si por ejemplo queremos modificar el índice T[2], podemos hacer uso del slicing.

T = (1, 2, 3, 4, 5)

T = T[:2] + (0,) + T[3:]

print(T) #(1, 2, 0, 4, 5)

Funciones en Python
• 📗📗 Funciones en Python

• 📙📙 Paso por valor y referencia

• 📙📙 Uso de args y kwargs

• 📙📙 Anotaciones en Funciones

• 📙📙 Funciones Lambda

• 📙📙 Recursividad

• 📕📕 Decoradores

• 📕📕 Generadores

• 📕📕 Corrutinas

• 📕📕 Caching Funciones

• 📕📕 Programación Funcional

Funciones en Python

Anteriormente hemos usado funciones nativas que vienen con Python como len() para calcular la
longitud de una lista, pero al igual que en otros lenguajes de programación, también podemos
definir nuestras propias funciones. Para ello hacemos uso de def.
def nombre_funcion(argumentos):

código

return retorno

Cualquier función tendrá un nombre, unos argumentos de entrada, un código a ejecutar y


unos parámetros de salida. Al igual que las funciones matemáticas, en programación nos permiten
realizar diferentes operaciones con la entrada, para entregar una determinada salida que dependerá del
código que escribamos dentro. Por lo tanto, es totalmente análogo al clásico y=f(x) de las matemáticas.

def f(x):

return 2*x

y = f(3)

print(y) # 6

Algo que diferencia en cierto modo las funciones en el mundo de la programación, es que no sólo
realizan una operación con sus entradas, sino que también parten de los siguientes principios:

• El principio de reusabilidad, que nos dice que si por ejemplo tenemos un fragmento de código
usado en muchos sitios, la mejor solución sería pasarlo a una función. Esto nos evitaría tener
código repetido, y que modificarlo fuera más fácil, ya que bastaría con cambiar la función una vez.

• Y el principio de modularidad, que defiende que en vez de escribir largos trozos de código, es
mejor crear módulos o funciones que agrupen ciertos fragmentos de código en funcionalidades
específicas, haciendo que el código resultante sea más fácil de leer.

Pasando argumentos de entrada

Empecemos por la función más sencilla de todas. Una función sin parámetros de entrada ni parámetros
de salida.

def di_hola():

print("Hola")

Hemos declarado o definido la función. El siguiente paso es llamarla con di_hola(). Si lo realizamos
veremos que se imprime Hola.

di_hola() # Hola

Vamos a complicar un poco las cosas pasando un argumento de entrada. Ahora si pasamos como
entrada un nombre, se imprimirá Hola y el nombre.

def di_hola(nombre):

print("Hola", nombre)

di_hola("Juan")
# Hola Juan

Python permite pasar argumentos también de otras formas. A continuación las explicamos todas.

Argumentos por posición

Los argumentos por posición o posicionales son la forma más básica e intuitiva de pasar parámetros. Si
tenemos una función resta() que acepta dos parámetros, se puede llamar como se muestra a
continuación.

def resta(a, b):

return a-b

resta(5, 3) # 2

Al tratarse de parámetros posicionales, se interpretará que el primer número es la a y el segundo la b. El


número de parámetros es fijo, por lo que si intentamos llamar a la función con solo uno, dará error.

#resta(1) # Error! TypeError

Tampoco es posible usar mas argumentos de los tiene la función definidos, ya que no sabría que hacer
con ellos. Por lo tanto si lo intentamos, Python nos dirá que toma 2 posicionales y estamos pasando 3, lo
que no es posible.

#TypeError: resta() takes 2 positional arguments but 3 were given

#resta(5,4,3) # Error

Argumentos por nombre

Otra forma de llamar a una función, es usando el nombre del argumento con = y su valor. El siguiente
código hace lo mismo que el código anterior, con la diferencia de que los argumentos no son
posicionales.

resta(a=3, b=5) # -2

Al indicar en la llamada a la función el nombre de la variable y el valor, el orden ya no importa, y se podría


llamar de la siguiente forma.

resta(b=5, a=3) # -2

Como es de esperar, si indicamos un argumento que no ha sido definido como parámetro de entrada,
tendremos un error.

#resta() got an unexpected keyword argument 'c'

#resta(b=5, c=3) # Error!

Argumentos por defecto


Tal vez queramos tener una función con algún parámetro opcional, que pueda ser usado o no
dependiendo de diferentes circunstancias. Para ello, lo que podemos hacer es asignar un valor por
defecto a la función. En el siguiente caso c valdría cero salvo que se indique lo contrario.

def suma(a, b, c=0):

return a+b+c

suma(5,5,3) # 13

Dado que el parámetro c tiene un valor por defecto, la función puede ser llamada sin ese valor.

suma(4,3) # 7

Podemos incluso asignar un valor por defecto a todos los parámetros, por lo que se podría llamar a la
función sin ningún argumento de entrada.

def suma(a=3, b=5, c=0):

return a+b+c

suma() # 8

Las siguientes llamadas a la función también son válidas

suma(1) #6

suma(4,5) # 9

suma(5,3,2) # 10

O haciendo uso de lo que hemos visto antes y usando los nombres de los argumentos.

suma(a=5, b=3) #8

Argumentos de longitud variable

En el ejemplo con argumentos por defecto, hemos visto que la función puede ser llamada con diferente
número de argumentos de entrada, pero esto no es realmente una función con argumentos de longitud
variable, ya que existe un número máximo.

Imaginemos que queremos una función suma() como la de antes, pero en este caso necesitamos que
sume todos los números de entrada que se le pasen, sin importar si son 3 o 100. Una primera forma de
hacerlo sería con una lista.

def suma(numeros):

total = 0

for n in numeros:

total += n

return total
suma([1,3,5,4]) # 13

La forma es válida y cumple nuestro requisito, pero realmente no estamos trabajando con argumentos
de longitud variable. En realidad tenemos un solo argumento que es una lista de números.

Por suerte, Python tiene una herramienta muy potente. Si declaramos un argumento con *, esto hará que
el argumento que se pase sea empaquetado en una tupla de manera automática. No confundir * con los
punteros en otros lenguajes de programación, no tiene nada que ver.

def suma(*numeros):

print(type(numeros))

# <class 'tuple'>

total = 0

for n in numeros:

total += n

return total

suma(1, 3, 5, 4) # 13

El resultado es igual que el anterior, y podemos ver como efectivamente numeros es de la clase tuple.
También podemos hacer otras llamadas con diferente número de argumentos

suma(6) # 6

suma(6, 4, 10) # 20

suma(6, 4, 10, 20, 4, 6, 7) # 57

Usando doble ** es posible también tener como parámetro de entrada una lista de elementos
almacenados en forma de clave y valor. En este caso podemos iterar los valores haciendo uso de items().

def suma(**kwargs):

suma = 0;

for key, value in kwargs.items():

print(key, value)

suma += value

return suma

suma(a=5, b=20, c=23) # 48

De igual manera, podemos pasar un diccionario como parámetro de entrada.


def suma(**kwargs):

suma = 0

for key, value in kwargs.items():

print(key, value)

suma += value

return suma

di = {'a': 10, 'b':20}

suma(**di) # 30

Sentencia return

El uso de la sentencia return permite realizar dos cosas:

• Salir de la función y transferir la ejecución de vuelta a donde se realizó la llamada.

• Devolver uno o varios parámetros, fruto de la ejecución de la función.

En lo relativo a lo primero, una vez se llama a return se para la ejecución de la función y se vuelve o
retorna al punto donde fue llamada. Es por ello por lo que el código que va después del return no es
ejecutado en el siguiente ejemplo.

def mi_funcion():

print("Entra en mi_funcion")

return

print("No llega")

mi_funcion() # Entra en mi_funcion

Por ello, sólo llamamos a return una vez hemos acabado de hacer lo que teníamos que hacer en la
función.

Por otro lado, se pueden devolver parámetros. Normalmente las funciones son llamadas para realizar
unos cálculos en base a una entrada, por lo que es interesante poder devolver ese resultado a quien
llamó a la función.

def di_hola():

return "Hola"

di_hola()

# 'Hola'

También es posible devolver mas de una variable, separadas por ,. En el siguiente ejemplo tenemos una
función que calcula la suma y media de tres números, y devuelve su resultado.
def suma_y_media(a, b, c):

suma = a+b+c

media = suma/3

return suma, media

suma, media = suma_y_media(9, 6, 3)

print(suma) # 18

print(media) # 6.0

Documentación

Ahora que ya tenemos nuestras propias funciones creadas, tal vez alguien se interese en ellas y
podamos compartírselas. Las funciones pueden ser muy complejas, y leer código ajeno no es tarea fácil.
Es por ello por lo que es importante documentar las funciones. Es decir, añadir comentarios para indicar
como deben ser usadas.

def mi_funcion_suma(a, b):

"""

Descripción de la función. Como debe ser usada,

que parámetros acepta y que devuelve

"""

return a+b

Para ello debemos usar la triple comilla """ al principio de la función. Se trata de una especie de
comentario que podemos usar para indicar como la función debe ser usada. No se trata de código, es un
simple comentario un tanto especial, conocido como docstring.

Ahora cualquier persona que tenga nuestra función, podrá llamar a la función help() y obtener la ayuda
de como debe ser usada.

help(mi_funcion_suma)

Otra forma de acceder a la documentación es la siguiente.

print(mi_funcion_suma.__doc__)

Para saber más: Las descripciones de las funciones suelen ser un poco mas detalladas de lo que
hemos mostrado. En la PEP257 se define en detalle como debería ser.

Anotaciones en funciones

Existe una funcionalidad relativamente reciente en Python llamada function annotation o anotaciones
en funciones. Dicha funcionalidad nos permite añadir metadatos a las funciones, indicando los tipos
esperados tanto de entrada como de salida.
def multiplica_por_3(numero: int) -> int:

return numero*3

multiplica_por_3(6) # 18

Las anotaciones son muy útiles de cara a la documentación del código, pero no imponen ninguna norma
sobre los tipos. Esto significa que se puede llamar a la función con un parámetro que no sea int, y no
obtendremos ningún error.

multiplica_por_3("Cadena")

# 'CadenaCadenaCadena'

Paso por valor y referencia

En muchos lenguajes de programación existen los conceptos de paso por valor y por referencia que
aplican a la hora de como trata una función a los parámetros que se le pasan como entrada. Su
comportamiento es el siguiente:

• Si usamos un parámetro pasado por valor, se creará una copia local de la variable, lo que implica
que cualquier modificación sobre la misma no tendrá efecto sobre la original.

• Con una variable pasada como referencia, se actuará directamente sobre la variable pasada, por
lo que las modificaciones afectarán a la variable original.

En Python las cosas son un poco distintas, y el comportamiento estará definido por el tipo de variable
con la que estamos tratando. Veamos un ejemplo de paso por valor.

x = 10

def funcion(entrada):

entrada = 0

funcion(x)

print(x) # 10

Iniciamos la x a 10 y se la pasamos a funcion(). Dentro de la función hacemos que la variable valga 0.


Dado que Python trata a los int como pasados por valor, dentro de la función se crea una copia local
de x, por lo que la variable original no es modificada.

No pasa lo mismo si por ejemplo x es una lista como en el siguiente ejemplo. En este caso Python lo trata
como si estuviese pasada por referencia, lo que hace que se modifique la variable original. La variable
original x ha sido modificada.

x = [10, 20, 30]


def funcion(entrada):

entrada.append(40)

funcion(x)

print(x) # [10, 20, 30, 40]

El ejemplo anterior nos podría llevar a pensar que si en vez de añadir un elemento a x, hacemos x=[],
estaríamos destruyendo la lista original. Sin embargo esto no es cierto.

x = [10, 20, 30]

def funcion(entrada):

entrada = []

funcion(x)

print(x)

# [10, 20, 30]

Una forma muy útil de saber lo que pasa por debajo de Python, es haciendo uso de la función id(). Esta
función nos devuelve un identificador único para cada objeto. Volviendo al primer ejemplo podemos ver
como los objetos a los que “apuntan” x y entrada son distintos.

x = 10

print(id(x)) # 4349704528

def funcion(entrada):

entrada = 0

print(id(entrada)) # 4349704208

funcion(x)

Sin embargo si hacemos lo mismo cuando la variable de entrada es una lista, podemos ver que en este
caso el objeto con el que se trabaja dentro de la función es el mismo que tenemos fuera.

x = [10, 20, 30]

print(id(x)) # 4422423560

def funcion(entrada):

entrada.append(40)
print(id(entrada)) # 4422423560

funcion(x)

Args y Kwargs en Python

Si alguna vez has tenido que definir una función con un número variable de argumentos y no has sabido
como hacerlo, a continuación te explicamos cómo gracias a los args y kwargs en Python.

Vamos a suponer que queremos una función que sume un conjunto de números, pero no sabemos a
priori la cantidad de números que se quieren sumar. Si por ejemplo tuviéramos tres, la función sería tan
sencilla como la siguiente.

def suma(a, b, c):

return a+b+c

suma(2, 4, 6)

#Salida: 12

El problema surge si por ejemplo queremos sumar cuatro números. Como es evidente, la siguiente
llamada a la función anterior daría un error ya que estamos usando cuatro argumentos mientras que la
función sólo soporta tres.

suma(2, 4, 6, 1)

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

Introducida ya la problemática, veamos como podemos resolver este problema con *args y **kwargs en
Python.

Uso de *args

Gracias a los *args en Python, podemos definir funciones cuyo número de argumentos es variable. Es
decir, podemos definir funciones genéricas que no aceptan un número determinado de parámetros, sino
que se “adaptan” al número de argumentos con los que son llamados.

De hecho, el args viene de arguments en Inglés, o argumentos. Haciendo uso de *args en la declaración
de la función podemos hacer que el número de parámetros que acepte sea variable.

def suma(*args):

s=0

for arg in args:

s += arg

return s
suma(1, 3, 4, 2)

#Salida 10

suma(1, 1)

#Salida 2

Antes de nada, el uso del nombre args es totalmente arbitrario, por lo que podrías haberlo llamado como
quisieras. Es una mera convención entre los usuarios de Python y resulta frecuente darle ese nombre. Lo
que si es un requisito, es usar * junto al nombre.

En el ejemplo anterior hemos visto como *args puede ser iterado, ya que en realidad es una tupla. Por lo
tanto iterando la tupla podemos acceder a todos los argumentos de entrada, y en nuestro caso sumarlos
y devolverlos.

Nótese que es un mero ejemplo didáctico. En realidad podríamos hacer algo como lo siguiente, lo que
sería mucho más sencillo.

def suma(*args):

return sum(args)

suma(5, 5, 3)

#Salida 13

Con esto resolvemos nuestro problema inicial, en el que necesitábamos un número variable de
argumentos. Sin embargo, hay otra forma que nos proporciona además un nombre asociado al
argumento, con el uso de **kwargs. La explicamos a continuación.

Uso de **kwargs

Al igual que en *args, en **kwargs el nombre es una mera convención entre los usuarios de Python.
Puedes usar cualquier otro nombre siempre y cuando respetes el **.

En este caso, en vez de tener una tupla tenemos un diccionario. Puedes verificarlo de la siguiente forma
con type().

def suma(**kwargs):

print(type(kwargs))

suma(x=3)

#<class 'dict'>
Pero veamos un ejemplo más completo. A diferencia de *args, los **kwargs nos permiten dar un nombre
a cada argumento de entrada, pudiendo acceder a ellos dentro de la función a través de un diccionario.

def suma(**kwargs):

s=0

for key, value in kwargs.items():

print(key, "=", value)

s += value

return s

suma(a=3, b=10, c=3)

#Salida

#a = 3

#b = 10

#c = 3

#16

Como podemos ver, es posible iterar los argumentos de entrada con items(), y podemos acceder a la
clave key (o nombre) y el valor o value de cada argumento.

El uso de los **kwargs es muy útil si además de querer acceder al valor de las variables dentro de la
función, quieres darles un nombre que de una información extra.

Mezclando *args y **kwargs

Una vez entendemos el uso de *args y **kwargs, podemos complicar las cosas un poco más. Es posible
mezclar argumentos normales con *args y **kwargs dentro de la misma función. Lo único que necesitas
saber es que debes definir la función en el siguiente orden:

• Primero argumentos normales.

• Después los *args.

• Y por último los **kwargs.

Veamos un ejemplo.

def funcion(a, b, *args, **kwargs):

print("a =", a)

print("b =", b)
for arg in args:

print("args =", arg)

for key, value in kwargs.items():

print(key, "=", value)

funcion(10, 20, 1, 2, 3, 4, x="Hola", y="Que", z="Tal")

#Salida

#a = 10

#b = 20

#args = 1

#args = 2

#args = 3

#args = 4

#x = Hola

#y = Que

#z = Tal

Y por último un truco que no podemos dejar sin mencionar es lo que se conoce como tuple unpacking.
Haciendo uso de *, podemos extraer los valores de una lista o tupla, y que sean pasados como
argumentos a la función.

def funcion(a, b, *args, **kwargs):

print("a =", a)

print("b =", b)

for arg in args:

print("args =", arg)

for key, value in kwargs.items():

print(key, "=", value)

args = [1, 2, 3, 4]

kwargs = {'x':"Hola", 'y':"Que", 'z':"Tal"}


funcion(10, 20, *args, **kwargs)

#Salida

#a = 10

#b = 20

#args = 1

#args = 2

#args = 3

#args = 4

#x = Hola

#y = Que

#z = Tal

Anotaciones en Funciones

Function Annotations en Python

Las anotaciones en funciones o function annotations de Python nos permiten añadir el tipo de los
argumentos de entrada y salida de una función. A continuación podemos ver un ejemplo con la
función suma(), que recibe dos argumentos a, b y cuyo tipo se espera que sea int.

def suma(a: int, b: int) -> int:

return a + b

print(suma(7, 3))

# Salida: 10

Sin embargo es muy importante notar que Python ignora las anotaciones. Es decir, son una mera nota
en el código que indica el tipo esperado, pero el siguiente código no daría ningún error. Más adelante
explicaremos cómo realizar el chequeo de tipos.

suma(7.0, 3.0)

Las anotaciones en funciones fueron introducidas en la PEP3107 para Python 3, y más adelante se
introdujo la PEP484 especificando la semántica que se debe usar.

Motivación y Necesidad de las Anotaciones

Python es un lenguaje de programación con tipado dinámico y duck typing, lo que significa que los tipos
(int, string, etc) le dan igual. Precisamente esto es lo que hace que el siguiente código funcione. La
función imprime puede ser llamada con cualquier tipo, ya que Python no realiza ninguna comprobación
del tipo de var.

def imprime(var):

print(var)

imprime(1.0) # 1.0

imprime(3) #3

imprime("Python") # Python

Sin embargo, en ciertas ocasiones esto nos puede traer problemas. ¿Y si queremos que la
función imprime sólo acepte que var sea de un tipo concreto? Pues bien, las anotaciones en
funciones o function annotations como acabamos de ver nos permiten especificar los tipos que se
esperan recibir.

Ejemplos de Function Annotations

Antes de nada es importante notar que las anotaciones en funciones no definen per se una semántica
propia. Es decir, podemos escribir lo que se nos ocurra después de cada argumento. Las anotaciones
pueden ser accedidas usando __annotations__.

def suma(a: 'parametro 1', b: 'parametro 2') -> 'retorno':

return a + b

print(suma.__annotations__)

# Salida:

# {'a': 'parametro 1',

# 'b': 'parametro 2',

# 'return': 'retorno'}

Aunque como hemos dicho se puedan realizar anotaciones arbitrarias, suele ser común usar tipos de
Python como int, str o float. En el siguiente ejemplo podemos ver como se combina una anotación con
un valor por defecto [].

def filtrar_pares(salida: 'list' = []) -> 'list':

return [i for i in salida if i%2 == 0]

print(filtrar_pares([1, 2, 3, 4, 5, 6]))
# Salida: [2, 4, 6]

También es posible usar como anotaciones clases definidas por nosotros, como ClaseA.

class ClaseA:

pass

def funcion(a: ClaseA) -> ClaseA:

return a

a = ClaseA()

funcion(a)

Por último, las anotaciones no están limitadas a los argumentos de las funciones, sino que también se
pueden asignar a variables que declaremos.

pi: float = 3.14

print(pi)

# Salida: 3.14

Sin embargo, como ya hemos visto anteriormente, Python no da error cuando los tipos no se
corresponden como en el ejemplo anterior.

# Ojo: No sería correcto, pero Python no da error

pi: int = 3.14

print(pi)

# Salida: 3.14

Entonces uno se puede preguntar, ¿y para que sirven las function annotation, si Python las ignora? Pues
bien, aunque las ignore, tenemos herramientas que nos permiten saber cuando no se están respetando.
Lo vemos a continuación con el análisis estático del código.

Uso de mypy y Static Type Checking

Una primera forma de verificar que las funciones se llaman con los parámetros especificados por las
anotaciones, sería lo siguiente. Sin embargo el error que obtendríamos sería en tiempo de ejecución. Es
decir, nos encontraríamos con el error una vez el código estuviera ejecutándose. Por lo tanto, no
recomendamos el uso del siguiente código.
# Nota: Ejemplo didáctico, no recomendado

def suma(a: int, b: int) -> int:

if isinstance(a, suma.__annotations__['a']) and isinstance(b, suma.__annotations__['b']):

return a + b

else:

raise Exception("Error de tipos")

print(suma(7, 3))

# Salida: 10

print(suma(7.0, 3.0))

# Salida: Exception: Error de tipos

Afortunadamente, tenemos herramientas como mypy que nos permiten hacer un chequeo estático de
los tipos, obteniendo el error antes de que el código se ejecute. Lo podemos instalar de la siguiente
manera.

$ pip instal mypy

Y volviendo al ejemplo anterior de la suma, podemos ver como el siguiente código si que pasaría los
checks de mypy.

# suma_correcta.py

def suma(a: int, b: int) -> int:

return a + b

print(suma(7, 3))

$ mypy suma_correcta.py

Success: no issues found in 1 source file

Sin embargo si cambiamos los tipos de los parámetros de entrada, obtendremos un error.

# suma_incorrecta.py

def suma(a: int, b: int) -> int:

return a + b
print(suma(7.0, "3"))

$ mypy suma_incorrecta.py

suma_incorrecta.py:5: error: Argument 1 to "suma" has incompatible type "float"; expected "int"

suma_incorrecta.py:5: error: Argument 2 to "suma" has incompatible type "str"; expected "int"

Found 2 errors in 1 file (checked 1 source file)

Como hemos indicado, la ventaja de mypy es que realiza un análisis estático, es decir, sin ejecutar el
código. Esto es algo muy importante ya que si de verdad queremos reforzar que se verifiquen los tipos,
no tendría mucho sentido hacerlo en tiempo de ejecución, ya que sería demasiado tarde y tendríamos
un error.

Funciones lambda

Las funciones lambda o anónimas son un tipo de funciones en Python que típicamente se definen en
una línea y cuyo código a ejecutar suele ser pequeño. Resulta complicado explicar las diferencias, y para
que te hagas una idea de ello te dejamos con la siguiente cita sacada de la documentación oficial.

“Python lambdas are only a shorthand notation if you’re too lazy to define a function.”

Lo que significa algo así como, “las funciones lambda son simplemente una versión acortada, que
puedes usar si te da pereza escribir una función”

Lo que sería una función que suma dos números como la siguiente.

def suma(a, b):

return a+b

Se podría expresar en forma de una función lambda de la siguiente manera.

lambda a, b : a + b

La primera diferencia es que una función lambda no tiene un nombre, y por lo tanto salvo que sea
asignada a una variable, es totalmente inútil. Para ello debemos.

suma = lambda a, b: a + b

Una vez tenemos la función, es posible llamarla como si de una función normal se tratase.

suma(2, 4)

Si es una función que solo queremos usar una vez, tal vez no tenga sentido almacenarla en una variable.
Es posible declarar la función y llamarla en la misma línea.

(lambda a, b: a + b)(2, 4)

## Ejemplos

Una función lambda puede ser la entrada a una función normal.


def mi_funcion(lambda_func):

return lambda_func(2,4)

mi_funcion(lambda a, b: a + b)

Y una función normal también puede ser la entrada de una función lambda. Nótese que son ejemplo
didácticos y sin demasiada utilidad práctica per se.

def mi_otra_funcion(a, b):

return a + b

(lambda a, b: mi_otra_funcion(a, b))(2, 4)

A pesar de que las funciones lambda tienen muchas limitaciones frente a las funciones normales,
comparten gran cantidad de funcionalidades. Es posible tener argumentos con valor asignado por
defecto.

(lambda a, b, c=3: a + b + c)(1, 2) # 6

También se pueden pasar los parámetros indicando su nombre.

(lambda a, b, c: a + b + c)(a=1, b=2, c=3) # 6

Al igual que en las funciones se puede tener un número variable de argumentos haciendo uso de *, lo
conocido como tuple unpacking.

(lambda *args: sum(args))(1, 2, 3) # 6

Y si tenemos los parámetros de entrada almacenados en forma de key y value como si fuera un
diccionario, también es posible llamar a la función.

(lambda **kwargs: sum(kwargs.values()))(a=1, b=2, c=3) # 6

Por último, es posible devolver más de un valor.

x = lambda a, b: (b, a)

print(x(3, 9))

# Salida (9,3)

Recursividad

¿En qué trabajas? Estoy intentando arreglar los problemas que creé cuando intentaba arreglar los
problemas que creé cuando intentaba arreglar los problemas que creé. Y así nació la recursividad.

La recursividad o recursión es un concepto que proviene de las matemáticas, y que aplicado al mundo
de la programación nos permite resolver problemas o tareas donde las mismas pueden ser divididas en
subtareas cuya funcionalidad es la misma. Dado que los subproblemas a resolver son de la misma
naturaleza, se puede usar la misma función para resolverlos. Dicho de otra manera, una función
recursiva es aquella que está definida en función de sí misma, por lo que se llama repetidamente a sí
misma hasta llegar a un punto de salida.

Cualquier función recursiva tiene dos secciones de código claramente divididas:

• Por un lado tenemos la sección en la que la función se llama a sí misma.

• Por otro lado, tiene que existir siempre una condición en la que la función retorna sin volver a
llamarse. Es muy importante porque de lo contrario, la función se llamaría de manera indefinida.

Veamos unos ejemplos con el factorial y la serie de fibonacci.

Calcular factorial

Uno de los ejemplos mas usados para entender la recursividad, es el cálculo del factorial de un
número n!. El factorial de un número n se define como la multiplicación de todos sus números
predecesores hasta llegar a uno. Por lo tanto 5!, leído como cinco factorial, sería 5*4*3*2*1.

Utilizando un enfoque tradicional no recursivo, podríamos calcular el factorial de la siguiente manera.

def factorial_normal(n):

r=1

i=2

while i <= n:

r *= i

i += 1

return r

factorial_normal(5) # 120

Dado que el factorial es una tarea que puede ser dividida en subtareas del mismo tipo
(multiplicaciones), podemos darle un enfoque recursivo y escribir la función de otra manera.

def factorial_recursivo(n):

if n == 1:

return 1

else:

return n * factorial_recursivo(n-1)
factorial_recursivo(5) # 120

Lo que realmente hacemos con el código anterior es llamar a la función factorial_recursivo() múltiples
veces. Es decir, 5! es igual a 5 * 4! y 4! es igual a 4 * 3! y así sucesivamente hasta llegar a 1.

Algo muy importante a tener en cuenta es que si realizamos demasiadas llamadas a la función,
podríamos llegar a tener un error del tipo RecursionError. Esto se debe a que todas las llamadas van
apilándose y creando un contexto de ejecución, algo que podría llegar a causar un stack overflow. Es por
eso por lo que Python lanza ese error, para protegernos de llegar a ese punto.

Serie de Fibonacci

Otro ejemplo muy usado para ilustrar las posibilidades de la recursividad, es calcular la serie de
fibonacci. Dicha serie calcula el elemento n sumando los dos anteriores n-1 + n-2. Se asume que los dos
primeros elementos son 0 y 1.

def fibonacci_recursivo(n):

if n == 0:

return 0

elif n == 1:

return 1

else:

return fibonacci_recursivo(n-1) + fibonacci_recursivo(n-2)

Podemos ver que siempre que la n sea distinta de cero o uno, se llamará recursivamente a la función,
buscando los dos elementos anteriores. Cuando la n valga cero o uno se dejará de llamar a la función y
se devolverá un valor concreto. Podemos calcular el elemento 7, que será 0,1,1,2,3,5,8,13, es decir, 13.

fibonacci_recursivo(7)

# 13

Decoradores

Los decoradores son funciones que modifican el comportamiento de otras funciones, ayudan a
acortar nuestro código y hacen que sea más Pythonic. Si alguna vez has visto @, estás ante un decorador
o decorator, bien sea uno que Python ofrece por defecto o uno que puede haber sido creado ex profeso.

Si aún no controlas las funciones te recomendamos que empieces con este post.

Veamos un ejemplo muy sencillo. Tenemos una función suma() que vamos a decorar
usando mi_decorador(). Para ello, antes de la declaración de la función suma, hacemos uso
de @mi_decorador.

def mi_decorador(funcion):

def nueva_funcion(a, b):


print("Se va a llamar")

c = funcion(a, b)

print("Se ha llamado")

return c

return nueva_funcion

@mi_decorador

def suma(a, b):

print("Entra en funcion suma")

return a + b

suma(5,8)

# Se va a llamar

# Entra en funcion suma

# Se ha llamado

Lo que realiza mi_decorador() es definir una nueva función que encapsula o envuelve la función que se
pasa como entrada. Concretamente lo que hace es hace uso de dos print(), uno antes y otro después de
la llamada la función.

Por lo tanto, cualquier función que use @mi_decorador tendrá dos print, uno y al principio y otro al final,
dando igual lo que realmente haga la función.

Veamos otro ejemplo usando el decorador sobre otra función.

@mi_decorador

def resta(a ,b):

print("Entra en funcion resta")

return a - b

resta(5, 3)

# Se va a llamar
# Entra en funcion resta

# Se ha llamado

Una vez entendido esto, vamos a entrar un poco más en detalle viendo como Python trata a las
funciones, como se puede definir un decorador sin @, como se pueden pasar argumentos a los
decoradores, y para finalizar, un ejemplo práctico.

Definiendo decoradores

Antes de nada hay que entender que todo en Python es un objeto, incluso una función. De hecho se
puede asignar una función a una variable. Nótese la diferencia entre:

• di_hola() llama a la función.

• di_hola hace referencia a la función, no la llama.

def di_hola():

print("Hola")

f1 = di_hola() # Llama a la función

f2 = di_hola # Asigna a f2 la función

print(f1) # None, di_hola no devuelve nada

print(f2) # <function di_hola at 0x1077bf158>

#f1() # Error! No es válido

f2() # Llama a f2, que es di_hola()

del f2 # Borra el objeto que es la función

#f2() # Error! Ya no existe

di_hola() # Ok. Sigue existiendo

Entendido esto, demos un paso más. En Python se pueden definir funciones dentro de otras funciones.
La función operaciones define suma() y resta(), y dependiendo del parámetro de entrada op, se devolverá
una u otra.

def operaciones(op):
def suma(a, b):

return a + b

def resta(a, b):

return a - b

if op == "suma":

return suma

elif op == "resta":

return resta

funcion_suma = operaciones("suma")

print(funcion_suma(5, 7)) # 12

funcion_suma = operaciones("resta")

print(funcion_suma(5, 7)) # -2

Si llamamos a la función devuelta con dos operandos, se realizará una operación distinta en función de
si se uso suma o resta.

Ahora ya podemos dar la última vuelta de tuerca y escribir nuestro propio decorador sin hacer uso
de @. Por un lado tenemos el decorador, que recibe como entrada una función y devuelve otra función
decorada. Por el otro la función suma() que queremos decorar.

def decorador(func):

def envoltorio_func(a, b):

print("Decorador antes de llamar a la función")

c = func(a, b)

print("Decorador después de llamar a la función")

return c

return envoltorio_func

def suma(a, b):

print("Dentro de suma")
return a + b

# Nueva funcion decorada

funcion_decorada = decorador(suma)

funcion_decorada(5, 8)

Entonces, haciendo uso de decorador y pasando como entrada suma, recibimos una nueva función
decorada con una funcionalidad nueva, lista para ser usada. Sería el equivalente al uso de @.

Decoradores con parámetros

Tal vez quieras que tu decorador tenga algún parámetro de entrada para modificar su comportamiento.
Se puede hacer envolviendo una vez más la función como se muestra a continuación.

def mi_decorador(arg):

def decorador_real(funcion):

def nueva_funcion(a, b):

print(arg)

c = funcion(a, b)

print(arg)

return c

return nueva_funcion

return decorador_real

@mi_decorador("Imprimer esto antes y después")

def suma(a, b):

print("Entra en funcion suma")

return a + b

suma(5,8)

# Imprimer esto antes y después

# Entra en funcion suma


# Imprimer esto antes y después

Es importante recalcar que los ejemplos mostrados hasta ahora son didácticos, y no tienen mucha
utilidad práctica. Veamos un ejemplo más práctico.

Ejemplo: logger

Una de las utilidades más usadas de los decoradores son los logger. Su uso nos permite escribir en un
fichero los resultados de ciertas operaciones, que funciones han sido llamadas, o cualquier información
que en un futuro resulte útil para ver que ha pasado.

En el siguiente ejemplo tenemos un uso más completo del decorador log() que escribe en un fichero los
resultados de las funciones que han sido llamadas.

def log(fichero_log):

def decorador_log(func):

def decorador_funcion(*args, **kwargs):

with open(fichero_log, 'a') as opened_file:

output = func(*args, **kwargs)

opened_file.write(f"{output}\n")

return decorador_funcion

return decorador_log

@log('ficherosalida.log')

def suma(a, b):

return a + b

@log('ficherosalida.log')

def resta(a, b):

return a - b

@log('ficherosalida.log')

def multiplicadivide(a, b, c):

return a*b/c
suma(10, 30)

resta(7, 23)

multiplicadivide(5, 10, 2)

Nótese que el decorador puede ser usado sobre funciones que tienen diferente número de parámetros
de entrada, y su funcionalidad será siempre la misma. Escribir en el fichero pasado como parámetro los
resultados de las operaciones.

Ejemplo: Uso autorizado

Otro caso de uso muy importante y ampliamente usado en Flask, que es un framework de desarrollo
web, es el uso de decoradores para asegurarse de que una función es llamada cuando el usuario se ha
autenticado.

Realizando alguna simplificación, podríamos tener un decorador que requiriera que una
variable autenticado fuera True. La función se ejecutará solo si dicha variable global es verdadera, y se
dará un error de lo contrario.

autenticado = True

def requiere_autenticación(f):

def funcion_decorada(*args, **kwargs):

if not autenticado:

print("Error. El usuario no se ha autenticado")

else:

return f(*args, **kwargs)

return funcion_decorada

@requiere_autenticación

def di_hola():

print("Hola")

di_hola()

Prueba a cambiar la variable de True a False.

Y esto es todo. En otros posts veremos el uso de functools y porque nos puede ser muy útil.
Generators en Python

Si alguna vez te has encontrado con una función en Python que no sólo tiene una sentencia return, sino
que además devuelve un valor haciendo uso de yield, ya has visto lo que es un generador o generator. A
continuación te explicamos cómo se crean, para qué sirven y sus ventajas. Es muy importante también
no confundir los generadores con las corrutinas, que también usan yield pero de otra manera, sin
embargo estas las dejamos para otro post.

Empecemos por lo básico. Seguramente ya sepas lo que es una función y cómo puede devolver un valor
con return.

def funcion():

return 5

Como hemos explicado, los generadores usan yield en vez de return. Vamos a ver que pasaría si
cambiamos el return por el yield.

def generador():

yield 5

Y ahora vamos a intentar llamar a las dos “funciones”.

print(funcion())

print(generador())

# Salida: 5

# Salida: <generator object generador at 0x103e7f0a0>

Podemos ver ya la primera diferencia al usar el yield. En el primer caso, se devuelve un 5, pero en el
segundo lo que se devuelve es en realidad un objeto de la clase generator. En realidad el número 5
también puede ser accedido en el caso del generador, pero esto lo veremos más adelante.

Entonces, si una función contiene al menos una sentencia yield, se convertirá en una función
generadora. Una función generadora se diferencia de una función normal en que tras ejecutar el yield, la
función devuelve el control a quién la llamó, pero la función es pausada y el estado (valor de las
variables) es guardado. Esto permite que su ejecución pueda ser reanudada más adelante.

Iterando los Generadores

A continuación vamos a ver cómo acceder a los valores del generador. Para entenderlo mejor, te
recomendamos que leas antes más acerca de iterables e iteradores.

Otra de las características que hacen a los generators diferentes, es que pueden ser iterados, ya que
codifican los métodos __iter__() y __next__(), por lo que podemos usar next() sobre ellos. Dado que son
iterables, lanzan también un StopIteration cuando se ha llegado al final.

Volviendo al ejemplo anterior, vamos a ver como podemos usar el next().


a = generador()

print(next(a))

# Salida: 5

Como te prometimos antes, el 5 también se podía acceder ¿has visto?. Pero vamos a ver que pasa ahora
si intentamos llamar otra vez a next(). Si recuerdas, sólo tenemos una llamada a yield.

a = generador()

print(next(a))

print(next(a))

# Salida: 5

# Salida: Error! StopIteration:

Como era de esperar, tenemos una excepción del tipo StopIteration, ya que el generador no devuelve
más valores. Esto se debe a que cada vez que usamos next() sobre el generador, se llama y se continúa
su ejecución después del último yield. Y en este caso cómo no hay más código, no se generan más
valores.

Creando Generadores

Vamos a ver otro ejemplo más completo donde tengamos un generador que genere varios valores. En la
siguiente función podemos ver como tenemos una variable n que incrementada en 1, y después retorna
con yield. Lo que pasará aquí, es que el generador generará un total de tres valores.

def generador():

n=1

yield n

n += 1

yield n

n += 1

yield n

Y haciendo uso de next() al igual que hacíamos antes, podemos ver los valores que han sido generados.
Lo que pasa por debajo, sería lo siguiente:

• Se entra en la función generadora, n=1 y se devuelve ese valor. Como ya hemos visto, el estado de
la función se guarda (el valor de n es guardado para la siguiente llamada)
• La segunda vez que usamos next() se entra otra vez en la función, pero se continúa su ejecución
donde se dejó anteriormente. Se suma 1 a la n y se devuelve el nuevo valor.

• La tercera llamada, realiza lo mismo.

• Una cuarta llamada daría un error, ya que no hay más código que ejecutar.

g = generador()

print(next(g))

print(next(g))

print(next(g))

# Salida: 1, 2, 3

Otra forma más cómoda de realizar lo mismo, sería usando un simple bucle for, ya que el generador es
iterable.

for i in generador():

print(i)

# Salida: 1, 2, 3

Forma alternativa

Los generadores también pueden ser creados de una forma mucho más sencilla y en una sola línea de
código. Su sintaxis es similar a las list comprehension, pero cambiando el corchete [] por paréntesis ().

El ejemplo con list comprehensions sería el siguiente. Simplemente se generan los valores de una lista
elevados al cuadrado.

lista = [2, 4, 6, 8, 10]

al_cuadrado = [x**2 for x in lista]

print(al_cuadrado)

# [4, 16, 36, 64, 100]

Y su equivalente con generadores sería lo siguiente.

al_cuadrado_generador = (x**2 for x in lista)

print(al_cuadrado_generador)

# Salida: <generator object <genexpr> at 0x103e803b8>

Y como hemos visto los valores pueden ser generados de la siguiente forma.

for i in al_cuadrado_generador:

print(i)
# Salda: 4, 16, 36, 64, 100

La diferencia entre el ejemplo usando list compregensions y generators es que en el caso de los
generadores, los valores no están almacenados en memoria, sino que se van generando al vuelo. Esta es
una de las principales ventajas de los generadores, ya que los elementos sólo son generados cuando se
piden, lo que hace que sean mucho más eficientes en lo relativo a la memoria.

Ventajas y ejemplos

Llegados a este punto tal vez te preguntes para qué sirven los generadores. Lo cierto es que aunque no
existieran, podría realizarse lo mismo creando una clase que implemente los
métodos __iter__() y __next__(). Veamos un ejemplo de una clase que genera los primeros 10 números
(0,9) al cuadrado.

class AlCuadrado:

def __init__(self):

self.i = 0

def __iter__(self):

return self

def __next__(self):

if self.i > 9:

raise StopIteration

result = self.i ** 2

self.i += 1

return result

Y de la misma forma que usábamos los generadores, podemos usar nuestra clase AlCuadrado. Creamos
un objeto de ella, y la iteramos. En cada iteración generará un nuevo valor nuevo hasta que se llegue al
final.

eleva_al_cuadrado = AlCuadrado()

for i in eleva_al_cuadrado:

print(i)

#0,1,4,9,16,25,36,49,64,81
Sin embargo esta forma es un tanto larga y tal vez confusa. Como hemos visto antes, podemos llegar a
hacer lo mismo en una sola línea de código. ¿Para que complicarse la vida?

Por otro lado, ya hemos mencionado que el uso de los generadores hace que no todos los valores estén
almacenados en memoria sino que sean generados al vuelo. Vamos a ver un ejemplo donde se puede ver
mejor. Supongamos que queremos sumar los primeros 100 números naturales (referencia). Una opción
podría ser crear una lista de todos ellos y después sumarla. En este caso, todos los valores son
almacenados en memoria, algo que podría ser un problema si por ejemplo intentamos sumar los
primeros 1e10 números.

def primerosn(n):

nums = []

for i in range(n):

nums.append(i)

return nums

print(sum(primerosn(100)))

# Salida: 4950

Sin embargo, podemos realizar lo mismo con un generador. En este caso los valores serán generados
uno por uno según se vayan necesitando.

def primerosn(n):

num = 0

for i in range(n):

yield num

num += 1

print(sum(primerosn(100)))

# Salida 4950

Nótese que es un ejemplo con fines didácticos, por lo que si quieres hacer esto, la mejor manera sería
usando un simple range() asumiendo que usas Python 3.

print(sum(range(100)))

# Salida: 4950

Corrutinas

Source code: Lib/asyncio/coroutines.py


Coroutines declarado con la sintaxis async/await es la forma preferida de escribir aplicaciones
asyncio. Por ejemplo, el siguiente fragmento de código imprime «hola», espera 1 segundo y luego
imprime «mundo»:

>>>

>>> import asyncio

>>> async def main():

... print('hello')

... await asyncio.sleep(1)

... print('world')

>>> asyncio.run(main())

hello

world

Tenga en cuenta que simplemente llamando a una corrutina no programará para que se ejecute:

>>>

>>> main()

<coroutine object main at 0x1053bb7c8>

Para ejecutar realmente una corutina, asyncio proporciona los siguientes mecanismos:

• La función asyncio.run() para ejecutar la función de punto de entrada de nivel superior


«main()» (consulte el ejemplo anterior.)

• Esperando en una corrutina. El siguiente fragmento de código imprimirá «hola» después de


esperar 1 segundo y luego imprimirá «mundo» después de esperar otros 2 segundos:

• import asyncio

• import time

• async def say_after(delay, what):

• await asyncio.sleep(delay)

• print(what)

• async def main():

• print(f"started at {time.strftime('%X')}")

• await say_after(1, 'hello')

• await say_after(2, 'world')

• print(f"finished at {time.strftime('%X')}")

• asyncio.run(main())

Salida esperada:

started at 17:13:52

hello

world

finished at 17:13:55

• La función asyncio.create_task() para ejecutar corrutinas concurrentemente como


asyncio Tasks.

Modifiquemos el ejemplo anterior y ejecutemos dos corrutinas say_after concurrentemente:

async def main():

task1 = asyncio.create_task(

say_after(1, 'hello'))

task2 = asyncio.create_task(

say_after(2, 'world'))

print(f"started at {time.strftime('%X')}")

# Wait until both tasks are completed (should take

# around 2 seconds.)
await task1

await task2

print(f"finished at {time.strftime('%X')}")

Tenga en cuenta que la salida esperada ahora muestra que el fragmento de código se ejecuta 1
segundo más rápido que antes:

started at 17:14:32

hello

world

finished at 17:14:34

• La clase asyncio.TaskGroup proporciona una alternativa más moderna a create_task().


Usando esta API, el último ejemplo se convierte en:

• async def main():

• async with asyncio.TaskGroup() as tg:

• task1 = tg.create_task(

• say_after(1, 'hello'))

• task2 = tg.create_task(

• say_after(2, 'world'))

• print(f"started at {time.strftime('%X')}")

• # The await is implicit when the context manager exits.

• print(f"finished at {time.strftime('%X')}")

El tiempo y la salida deben ser los mismos que para la versión anterior.

Added in version 3.11: asyncio.TaskGroup.

Esperables

Decimos que un objeto es un objeto esperable si se puede utilizar en una expresión await. Muchas
API de asyncio están diseñadas para aceptar los valores esperables.
Hay tres tipos principales de objetos esperables: corrutinas, Tareas y Futures.

Corrutinas

Las corrutinas de Python son esperables y por lo tanto se pueden esperar de otras corrutinas:

import asyncio

async def nested():

return 42

async def main():

# Nothing happens if we just call "nested()".

# A coroutine object is created but not awaited,

# so it *won't run at all*.

nested() # will raise a "RuntimeWarning".

# Let's do it differently now and await it:

print(await nested()) # will print "42".

asyncio.run(main())

Importante

En esta documentación se puede utilizar el término «corrutina» para dos conceptos estrechamente
relacionados:

• una función corrutina: una función async def;

• un objeto corrutina: un objeto retornado llamando a una función corrutina.

Tareas

Las tareas se utilizan para programar corrutinas concurrentemente.

Cuando una corrutina se envuelve en una Tarea con funciones como asyncio.create_task() la
corrutina se programa automáticamente para ejecutarse pronto:

import asyncio
async def nested():

return 42

async def main():

# Schedule nested() to run soon concurrently

# with "main()".

task = asyncio.create_task(nested())

# "task" can now be used to cancel "nested()", or

# can simply be awaited to wait until it is complete:

await task

asyncio.run(main())

Futures

Un Future es un objeto esperable especial de bajo-nivel que representa un resultado eventual de


una operación asíncrona.

Cuando un objeto Future es esperado significa que la corrutina esperará hasta que el Future se
resuelva en algún otro lugar.

Los objetos Future de asyncio son necesarios para permitir que el código basado en retro llamada
se use con async/await.

Normalmente , no es necesario crear objetos Future en el código de nivel de aplicación.

Los objetos Future, a veces expuestos por bibliotecas y algunas API de asyncio, pueden ser
esperados:

async def main():

await function_that_returns_a_future_object()

# this is also valid:

await asyncio.gather(

function_that_returns_a_future_object(),
some_python_coroutine()

Un buen ejemplo de una función de bajo nivel que retorna un objeto Future
es loop.run_in_executor().

Creando Tareas

Source code: Lib/asyncio/tasks.py

asyncio.create_task(coro, *, name=None, context=None)

Envuelve una coroutine coro en una Task y programa su ejecución. Retorna el objeto Tarea.

Si name no es None, se establece como el nombre de la tarea mediante Task.set_name().

Un argumento context opcional de solo palabra clave permite especificar


un contextvars.Context personalizado para que se ejecute el coro. La copia de contexto actual se
crea cuando no se proporciona context.

La tarea se ejecuta en el bucle retornado por get_running_loop(), RuntimeError se genera si no hay


ningún bucle en ejecución en el subproceso actual.

Nota

asyncio.TaskGroup.create_task() es una alternativa más nueva que permite una espera conveniente
para un grupo de tareas relacionadas.

Importante

Guarde una referencia al resultado de esta función, para evitar que una tarea desaparezca en
medio de la ejecución. El bucle de eventos solo mantiene referencias débiles a las tareas. Una
tarea a la que no se hace referencia en ningún otro lugar puede ser recolectada por el recolector de
basura en cualquier momento, incluso antes de que se complete. Para tareas confiables en
segundo plano, de tipo «lanzar y olvidar», reúnalas en una colección:

background_tasks = set()

for i in range(10):

task = asyncio.create_task(some_coro(param=i))

# Add task to the set. This creates a strong reference.


background_tasks.add(task)

# To prevent keeping references to finished tasks forever,

# make each task remove its own reference from the set after

# completion:

task.add_done_callback(background_tasks.discard)

Added in version 3.7.

Distinto en la versión 3.8: Se ha añadido el parámetro name.

Distinto en la versión 3.11: Se ha añadido el parámetro context.

Cancelación de tareas

Las tareas se pueden cancelar de forma fácil y segura. Cuando se cancela una tarea, se
generará asyncio.CancelledError en la tarea en la próxima oportunidad.

Se recomienda que las corrutinas utilicen bloques try/finally para realizar de forma sólida la lógica
de limpieza. En caso de que asyncio.CancelledError se detecte explícitamente, generalmente
debería propagarse cuando se complete la limpieza. asyncio.CancelledError subclasifica
directamente a BaseException, por lo que la mayor parte del código no necesitará tenerlo en
cuenta.

Los componentes asyncio que permiten la simultaneidad estructurada,


como asyncio.TaskGroup y asyncio.timeout(), se implementan mediante cancelación internamente
y podrían comportarse mal si una rutina traga asyncio.CancelledError. De manera similar, el código
de usuario generalmente no debería llamar a uncancel. Sin embargo, en los casos en los que
realmente se desea suprimir asyncio.CancelledError, es necesario llamar también
a uncancel() para eliminar completamente el estado de cancelación.

Grupos de tareas

Los grupos de tareas combinan una API de creación de tareas con una forma conveniente y
confiable de esperar a que finalicen todas las tareas del grupo.

class asyncio.TaskGroup

Un asynchronous context manager que contiene un grupo de tareas. Las tareas se pueden agregar
al grupo usando create_task(). Se esperan todas las tareas cuando sale el administrador de
contexto.

Added in version 3.11.

create_task(coro, *, name=None, context=None)


Create a task in this task group. The signature matches that of asyncio.create_task(). If the task
group is inactive (e.g. not yet entered, already finished, or in the process of shutting down), we will
close the given coro.

Distinto en la versión 3.13: Close the given coroutine if the task group is not active.

Ejemplo:

async def main():

async with asyncio.TaskGroup() as tg:

task1 = tg.create_task(some_coro(...))

task2 = tg.create_task(another_coro(...))

print(f"Both tasks have completed now: {task1.result()}, {task2.result()}")

La instrucción async with esperará a que finalicen todas las tareas del grupo. Mientras espera, aún
se pueden agregar nuevas tareas al grupo (por ejemplo, pasando tg a una de las corrutinas y
llamando a tg.create_task() en esa corrutina). Una vez finalizada la última tarea y salido del
bloque async with, no se podrán añadir nuevas tareas al grupo.

La primera vez que alguna de las tareas pertenecientes al grupo falla con una excepción que no
sea asyncio.CancelledError, las tareas restantes del grupo se cancelan. No se pueden añadir más
tareas al grupo. En este punto, si el cuerpo de la instrucción async with aún está activo (es decir,
aún no se ha llamado a __aexit__()), la tarea que contiene directamente la
instrucción async with también se cancela. El asyncio.CancelledError resultante interrumpirá
un await, pero no saldrá de la instrucción async with que lo contiene.

Una vez que todas las tareas han finalizado, si alguna tarea ha fallado con una excepción que no
sea asyncio.CancelledError, esas excepciones se combinan en
un ExceptionGroup o BaseExceptionGroup (según corresponda; consulte su documentación) que
luego se genera.

Dos excepciones básicas se tratan de manera especial: si alguna tarea falla


con KeyboardInterrupt o SystemExit, el grupo de tareas aún cancela las tareas restantes y las
espera, pero luego se vuelve a generar el KeyboardInterrupt o SystemExit inicial en lugar
de ExceptionGroup o BaseExceptionGroup.

Si el cuerpo de la instrucción async with finaliza con una excepción (por lo que se llama
a __aexit__() con un conjunto de excepciones), esto se trata igual que si una de las tareas fallara: las
tareas restantes se cancelan y luego se esperan, y las excepciones de no cancelación se agrupan
en un grupo de excepción y se generan. La excepción pasada a __aexit__(), a menos que
sea asyncio.CancelledError, también se incluye en el grupo de excepciones. Se hace el mismo caso
especial para KeyboardInterrupt y SystemExit que en el párrafo anterior.

Task groups are careful not to mix up the internal cancellation used to «wake up»
their __aexit__() with cancellation requests for the task in which they are running made by other
parties. In particular, when one task group is syntactically nested in another, and both experience
an exception in one of their child tasks simultaneously, the inner task group will process its
exceptions, and then the outer task group will receive another cancellation and process its own
exceptions.

In the case where a task group is cancelled externally and also must raise an ExceptionGroup, it will
call the parent task’s cancel() method. This ensures that a asyncio.CancelledError will be raised at
the next await, so the cancellation is not lost.

Task groups preserve the cancellation count reported by asyncio.Task.cancelling().

Distinto en la versión 3.13: Improved handling of simultaneous internal and external cancellations
and correct preservation of cancellation counts.

Terminating a Task Group

While terminating a task group is not natively supported by the standard library, termination can be
achieved by adding an exception-raising task to the task group and ignoring the raised exception:

import asyncio

from asyncio import TaskGroup

class TerminateTaskGroup(Exception):

"""Exception raised to terminate a task group."""

async def force_terminate_task_group():

"""Used to force termination of a task group."""

raise TerminateTaskGroup()

async def job(task_id, sleep_time):

print(f'Task {task_id}: start')

await asyncio.sleep(sleep_time)

print(f'Task {task_id}: done')

async def main():

try:

async with TaskGroup() as group:


# spawn some tasks

group.create_task(job(1, 0.5))

group.create_task(job(2, 1.5))

# sleep for 1 second

await asyncio.sleep(1)

# add an exception-raising task to force the group to terminate

group.create_task(force_terminate_task_group())

except* TerminateTaskGroup:

pass

asyncio.run(main())

Expected output:

Task 1: start

Task 2: start

Task 1: done

Durmiendo

coroutine asyncio.sleep(delay, result=None)

Bloquea por delay segundos.

Si se proporciona result, se retorna al autor de la llamada cuando se completa la corrutina.

sleep() siempre suspende la tarea actual, permitiendo que se ejecuten otras tareas.

Establecer el retraso en 0 proporciona una ruta optimizada para permitir que se ejecuten otras
tareas. Esto puede ser utilizado por funciones de ejecución prolongada para evitar bloquear el
bucle de eventos durante toda la duración de la llamada a la función.

Ejemplo de una rutina que muestra la fecha actual cada segundo durante 5 segundos:

import asyncio

import datetime

async def display_date():

loop = asyncio.get_running_loop()
end_time = loop.time() + 5.0

while True:

print(datetime.datetime.now())

if (loop.time() + 1.0) >= end_time:

break

await asyncio.sleep(1)

asyncio.run(display_date())

Distinto en la versión 3.10: Se quitó el parámetro loop.

Distinto en la versión 3.13: Raises ValueError if delay is nan.

Ejecutando tareas concurrentemente

awaitable asyncio.gather(*aws, return_exceptions=False)

Ejecute objetos esperables en la secuencia aws de forma concurrently.

Si cualquier esperable en aws es una corrutina, se programa automáticamente como una Tarea.

Si todos los esperables se completan correctamente, el resultado es una lista agregada de valores
retornados. El orden de los valores de resultado corresponde al orden de esperables en aws.

Si return_exceptions es False (valor predeterminado), la primera excepción provocada se propaga


inmediatamente a la tarea que espera en gather(). Otros esperables en la secuencia aws no se
cancelarán y continuarán ejecutándose.

Si return_exceptions es True, las excepciones se tratan igual que los resultados correctos y se
agregan en la lista de resultados.

Si gather() es cancelado, todos los esperables enviados (que aún no se han completado) también
se cancelan.

Si alguna Tarea o Future de la secuencia aws se cancela, se trata como si se


lanzara CancelledError – la llamada gather() no se cancela en este caso. Esto es para evitar la
cancelación de una Tarea/Future enviada para hacer que otras Tareas/Futures sean canceladas.

Nota

Una forma más moderna de crear y ejecutar tareas simultáneamente y esperar a que se completen
es asyncio.TaskGroup.

Ejemplo:
import asyncio

async def factorial(name, number):

f=1

for i in range(2, number + 1):

print(f"Task {name}: Compute factorial({number}), currently i={i}...")

await asyncio.sleep(1)

f *= i

print(f"Task {name}: factorial({number}) = {f}")

return f

async def main():

# Schedule three calls *concurrently*:

L = await asyncio.gather(

factorial("A", 2),

factorial("B", 3),

factorial("C", 4),

print(L)

asyncio.run(main())

# Expected output:

# Task A: Compute factorial(2), currently i=2...

# Task B: Compute factorial(3), currently i=2...

# Task C: Compute factorial(4), currently i=2...

# Task A: factorial(2) = 2

# Task B: Compute factorial(3), currently i=3...


# Task C: Compute factorial(4), currently i=3...

# Task B: factorial(3) = 6

# Task C: Compute factorial(4), currently i=4...

# Task C: factorial(4) = 24

# [2, 6, 24]

Nota

If return_exceptions is false, cancelling gather() after it has been marked done won’t cancel any
submitted awaitables. For instance, gather can be marked done after propagating an exception to
the caller, therefore, calling gather.cancel() after catching an exception (raised by one of the
awaitables) from gather won’t cancel any other awaitables.

Distinto en la versión 3.7: Si se cancela el propio gather, la cancelación se propaga


independientemente de return_exceptions.

Distinto en la versión 3.10: Se quitó el parámetro loop.

Obsoleto desde la versión 3.10: Se emite una advertencia de obsolescencia si no se proporcionan


argumentos posicionales o no todos los argumentos posicionales son objetos de tipo Future y no
hay un bucle de eventos en ejecución.

Fábrica de tareas ansiosas

asyncio.eager_task_factory(loop, coro, *, name=None, context=None)

Una fábrica de tareas para una ejecución entusiasta de tareas.

Cuando se utiliza esta fábrica (a través de loop.set_task_factory(asyncio.eager_task_factory)), las


corrutinas comienzan a ejecutarse sincrónicamente durante la construcción de Task. Las tareas
sólo se programan en el bucle de eventos si se bloquean. Esto puede suponer una mejora del
rendimiento, ya que se evita la sobrecarga de la programación del bucle para las corrutinas que se
completan sincrónicamente.

Un ejemplo común en el que esto resulta beneficioso son las rutinas que emplean almacenamiento
en caché o memorización para evitar E/S reales cuando sea posible.

Nota

La ejecución inmediata de la corrutina es un cambio semántico. Si la rutina regresa o se activa, la


tarea nunca se programa en el bucle de eventos. Si la ejecución de la rutina se bloquea, la tarea se
programa en el bucle de eventos. Este cambio puede introducir cambios de comportamiento en las
aplicaciones existentes. Por ejemplo, es probable que cambie el orden de ejecución de las tareas
de la aplicación.
Added in version 3.12.

asyncio.create_eager_task_factory(custom_task_constructor)

Cree una fábrica de tareas entusiastas, similar a eager_task_factory(), utilizando


el custom_task_constructor proporcionado al crear una nueva tarea en lugar
del Task predeterminado.

custom_task_constructor debe ser un callable con la firma que coincida con la firma
de Task.__init__. El invocable debe devolver un objeto compatible con asyncio.Task.

Esta función devuelve un callable destinado a ser utilizado como fábrica de tareas de un bucle de
eventos a través de loop.set_task_factory(factory)).

Added in version 3.12.

Protección contra cancelación

awaitable asyncio.shield(aw)

Protege un objeto esperable de ser cancelado.

Si aw es una corrutina, se programa automáticamente como una Tarea.

La declaración:

task = asyncio.create_task(something())

res = await shield(task)

es equivalente a:

res = await something()

excepto que si la corrutina que lo contiene se cancela, la tarea que se ejecuta en something() no se
cancela. Desde el punto de vista de something(), la cancelación no ocurrió. Aunque su invocador
siga cancelado, por lo que la expresión «await» sigue generando un CancelledError.

Si something() se cancela por otros medios (es decir, desde dentro de sí mismo) eso también
cancelaría shield().

Si se desea ignorar por completo la cancelación (no se recomienda) la función shield() debe
combinarse con una cláusula try/except, como se indica a continuación:

task = asyncio.create_task(something())

try:

res = await shield(task)

except CancelledError:

res = None
Importante

Guarde una referencia a las tareas pasadas a esta función, para evitar que una tarea desaparezca a
mitad de la ejecución. El bucle de eventos solo mantiene referencias débiles a las tareas. Una tarea
a la que no se hace referencia en ningún otro lugar puede ser recolectada por el recolector de
basura en cualquier momento, incluso antes de que se complete.

Distinto en la versión 3.10: Se quitó el parámetro loop.

Obsoleto desde la versión 3.10: Se emite una advertencia de obsolescencia si aw no es un objeto


similares a Futures y no hay un bucle de eventos en ejecución.

Tiempo agotado

asyncio.timeout(delay)

Un asynchronous context manager que se puede usar para limitar la cantidad de tiempo que se
pasa esperando algo.

delay puede ser None o un número flotante/int de segundos de espera. Si delay es None, no se
aplicará ningún límite de tiempo; esto puede ser útil si se desconoce el retraso cuando se crea el
administrador de contexto.

En cualquier caso, el administrador de contexto se puede reprogramar después de la creación


mediante Timeout.reschedule().

Ejemplo:

async def main():

async with asyncio.timeout(10):

await long_running_task()

Si long_running_task tarda más de 10 segundos en completarse, el administrador de contexto


cancelará la tarea actual y manejará internamente el asyncio.CancelledError resultante,
transformándolo en un asyncio.TimeoutError que se puede capturar y manejar.

Nota

El administrador de contexto asyncio.timeout() es lo que transforma el asyncio.CancelledError en


un asyncio.TimeoutError, lo que significa que el asyncio.TimeoutError solo puede
capturarse outside del administrador de contexto.

Ejemplo de captura de TimeoutError:

async def main():

try:
async with asyncio.timeout(10):

await long_running_task()

except TimeoutError:

print("The long operation timed out, but we've handled it.")

print("This statement will run regardless.")

El administrador de contexto producido por asyncio.timeout() puede reprogramarse para una fecha
límite diferente e inspeccionarse.

class asyncio.Timeout(when)

Un asynchronous context manager para cancelar corrutinas vencidas.

when debe ser un tiempo absoluto en el que el contexto debe expirar, según lo medido por el reloj
del bucle de eventos:

• Si when es None, el tiempo de espera nunca se activará.

• Si when < loop.time(), el tiempo de espera se activará en la próxima iteración del bucle de
eventos

when() → float | None

Retorna la fecha límite actual, o None si la fecha límite actual no está establecida.

reschedule(when: float | None)

Reprogramar el tiempo de espera.

expired() → bool

Retorna si el administrador de contexto ha excedido su fecha límite (caducada).

Ejemplo:

async def main():

try:

# We do not know the timeout when starting, so we pass ``None``.

async with asyncio.timeout(None) as cm:

# We know the timeout now, so we reschedule it.

new_deadline = get_running_loop().time() + 10

cm.reschedule(new_deadline)
await long_running_task()

except TimeoutError:

pass

if cm.expired():

print("Looks like we haven't finished on time.")

Los administradores de contexto de tiempo de espera se pueden anidar de forma segura.

Added in version 3.11.

asyncio.timeout_at(when)

Similar a asyncio.timeout(), excepto que when es el tiempo absoluto para dejar de esperar, o None.

Ejemplo:

async def main():

loop = get_running_loop()

deadline = loop.time() + 20

try:

async with asyncio.timeout_at(deadline):

await long_running_task()

except TimeoutError:

print("The long operation timed out, but we've handled it.")

print("This statement will run regardless.")

Added in version 3.11.

coroutine asyncio.wait_for(aw, timeout)

Espere a que el aw esperable se complete con un tiempo agotado.

Si aw es una corrutina, se programa automáticamente como una Tarea.

timeout puede ser None o punto flotante o un número entero de segundos a esperar.
Si timeout es None, se bloquea hasta que Future se completa.

Si se agota el tiempo de espera, cancela la tarea y lanza TimeoutError.

Para evitar la cancelación de la tarea , envuélvala en shield().


La función esperará hasta que se cancele el Future, por lo que el tiempo de espera total puede
exceder el timeout. Si ocurre una excepción durante la cancelación, se propaga.

Si se cancela la espera, el Future aw también se cancela.

Ejemplo:

async def eternity():

# Sleep for one hour

await asyncio.sleep(3600)

print('yay!')

async def main():

# Wait for at most 1 second

try:

await asyncio.wait_for(eternity(), timeout=1.0)

except TimeoutError:

print('timeout!')

asyncio.run(main())

# Expected output:

# timeout!

Distinto en la versión 3.7: Cuando se cancela aw debido a un tiempo de espera, wait_for espera a
que se cancele aw. Anteriormente, lanzaba TimeoutError inmediatamente.

Distinto en la versión 3.10: Se quitó el parámetro loop.

Distinto en la versión 3.11: Raises TimeoutError instead of asyncio.TimeoutError.

Esperando primitivas

coroutine asyncio.wait(aws, *, timeout=None, return_when=ALL_COMPLETED)

Ejecuta instancias Future y Task en el iterable aws simultáneamente y bloquea hasta la condición
especificada por return_when.

El iterable aws no debe estar vacío.


Retorna dos conjuntos de Tareas/Futures: (done, pending).

Uso:

done, pending = await asyncio.wait(aws)

timeout (un punto flotante o int), si se especifica, se puede utilizar para controlar el número
máximo de segundos que hay que esperar antes de retornar.

Tenga en cuenta que esta función no lanza TimeoutError. Los futuros o las tareas que no se realizan
cuando se agota el tiempo de espera simplemente se retornan en el segundo conjunto.

return_when indica cuándo debe retornar esta función. Debe ser una de las siguientes constantes:

Constante Descripción

La función retornará cuando cualquier Future termine o se


asyncio.FIRST_COMPLETED
cancele.

The function will return when any future finishes by raising


asyncio.FIRST_EXCEPTION an exception. If no future raises an exception then it is
equivalent to ALL_COMPLETED.

La función retornará cuando todos los Futures terminen o se


asyncio.ALL_COMPLETED
cancelen.

A diferencia de wait_for(), wait() no cancela los Futures cuando se produce un agotamiento de


tiempo.

Distinto en la versión 3.10: Se quitó el parámetro loop.

Distinto en la versión 3.11: Está prohibido pasar objetos de rutina a wait() directamente.

Distinto en la versión 3.12: Se agregó soporte para generadores que generan tareas.

asyncio.as_completed(aws, *, timeout=None)

Run awaitable objects in the aws iterable concurrently. The returned object can be iterated to
obtain the results of the awaitables as they finish.

The object returned by as_completed() can be iterated as an asynchronous iterator or a


plain iterator. When asynchronous iteration is used, the originally-supplied awaitables are yielded if
they are tasks or futures. This makes it easy to correlate previously-scheduled tasks with their
results. Example:

ipv4_connect = create_task(open_connection("127.0.0.1", 80))

ipv6_connect = create_task(open_connection("::1", 80))

tasks = [ipv4_connect, ipv6_connect]


async for earliest_connect in as_completed(tasks):

# earliest_connect is done. The result can be obtained by

# awaiting it or calling earliest_connect.result()

reader, writer = await earliest_connect

if earliest_connect is ipv6_connect:

print("IPv6 connection established.")

else:

print("IPv4 connection established.")

During asynchronous iteration, implicitly-created tasks will be yielded for supplied awaitables that
aren’t tasks or futures.

When used as a plain iterator, each iteration yields a new coroutine that returns the result or raises
the exception of the next completed awaitable. This pattern is compatible with Python versions
older than 3.13:

ipv4_connect = create_task(open_connection("127.0.0.1", 80))

ipv6_connect = create_task(open_connection("::1", 80))

tasks = [ipv4_connect, ipv6_connect]

for next_connect in as_completed(tasks):

# next_connect is not one of the original task objects. It must be

# awaited to obtain the result value or raise the exception of the

# awaitable that finishes next.

reader, writer = await next_connect

A TimeoutError is raised if the timeout occurs before all awaitables are done. This is raised by
the async for loop during asynchronous iteration or by the coroutines yielded during plain iteration.

Distinto en la versión 3.10: Se quitó el parámetro loop.

Obsoleto desde la versión 3.10: Se emite una advertencia de obsolescencia si no todos los objetos
en espera en el iterable aws son objetos de tipo Future y no hay un bucle de eventos en ejecución.

Distinto en la versión 3.12: Se agregó soporte para generadores que generan tareas.
Distinto en la versión 3.13: The result can now be used as either an asynchronous iterator or as a
plain iterator (previously it was only a plain iterator).

Ejecutando en hilos

coroutine asyncio.to_thread(func, /, *args, **kwargs)

Ejecutar asincrónicamente la función func en un hilo separado.

Cualquier *args y **kwargs suministrados para esta función se pasan directamente a func. Además,
el current contextvars.Context se propaga, lo que permite acceder a las variables de contexto del
subproceso del bucle de eventos en el subproceso separado.

Retorna una corrutina que se puede esperar para obtener el resultado final de func.

Esta función de rutina está diseñada principalmente para ejecutar funciones/métodos vinculados a
IO que, de otro modo, bloquearían el bucle de eventos si se ejecutaran en el subproceso principal.
Por ejemplo:

def blocking_io():

print(f"start blocking_io at {time.strftime('%X')}")

# Note that time.sleep() can be replaced with any blocking

# IO-bound operation, such as file operations.

time.sleep(1)

print(f"blocking_io complete at {time.strftime('%X')}")

async def main():

print(f"started main at {time.strftime('%X')}")

await asyncio.gather(

asyncio.to_thread(blocking_io),

asyncio.sleep(1))

print(f"finished main at {time.strftime('%X')}")

asyncio.run(main())
# Expected output:

# started main at 19:50:53

# start blocking_io at 19:50:53

# blocking_io complete at 19:50:54

# finished main at 19:50:54

Llamar directamente a blocking_io() en cualquier rutina bloquearía el bucle de eventos durante su


duración, lo que daría como resultado 1 segundo adicional de tiempo de ejecución. En cambio, al
usar asyncio.to_thread(), podemos ejecutarlo en un subproceso separado sin bloquear el ciclo de
eventos.

Nota

Debido a GIL, asyncio.to_thread() generalmente solo se puede usar para hacer que las funciones
vinculadas a IO no bloqueen. Sin embargo, para los módulos de extensión que lanzan GIL o
implementaciones alternativas de Python que no tienen uno, asyncio.to_thread() también se puede
usar para funciones vinculadas a la CPU.

Added in version 3.9.

Planificación desde otros hilos

asyncio.run_coroutine_threadsafe(coro, loop)

Envía una corrutina al bucle de eventos especificado. Seguro para Hilos.

Retorna concurrent.futures.Future para esperar el resultado de otro hilo del SO (Sistema


Operativo).

Esta función está pensada para llamarse desde un hilo del SO diferente al que se ejecuta el bucle
de eventos. Ejemplo:

# Create a coroutine

coro = asyncio.sleep(1, result=3)

# Submit the coroutine to a given loop

future = asyncio.run_coroutine_threadsafe(coro, loop)


# Wait for the result with an optional timeout argument

assert future.result(timeout) == 3

Si se lanza una excepción en la corrutina, el Future retornado será notificado. También se puede
utilizar para cancelar la tarea en el bucle de eventos:

try:

result = future.result(timeout)

except TimeoutError:

print('The coroutine took too long, cancelling the task...')

future.cancel()

except Exception as exc:

print(f'The coroutine raised an exception: {exc!r}')

else:

print(f'The coroutine returned: {result!r}')

Consulte la sección de la documentación Concurrencia y multi hilos.

A diferencia de otras funciones asyncio, esta función requiere que el argumento loop se pase
explícitamente.

Added in version 3.5.1.

Introspección

asyncio.current_task(loop=None)

Retorna la instancia Task actualmente en ejecución o None si no se está ejecutando ninguna tarea.

Si loop es None get_running_loop() se utiliza para obtener el bucle actual.

Added in version 3.7.

asyncio.all_tasks(loop=None)

Retorna un conjunto de objetos Task que se ejecutan por el bucle.

Si loop es None, get_running_loop() se utiliza para obtener el bucle actual.

Added in version 3.7.

asyncio.iscoroutine(obj)

Retorna True si obj es un objeto corutina.

Added in version 3.4.


Objeto Task

class asyncio.Task(coro, *, loop=None, name=None, context=None, eager_start=False)

Un objeto similar a Future que ejecuta Python coroutine. No es seguro hilos.

Las tareas se utilizan para ejecutar corrutinas en bucles de eventos. Si una corrutina aguarda en un
Future, la Tarea suspende la ejecución de la corrutina y espera la finalización del Future. Cuando el
Future termina, se reanuda la ejecución de la corrutina envuelta.

Los bucles de eventos usan la programación cooperativa: un bucle de eventos ejecuta una tarea a
la vez. Mientras una Tarea espera para la finalización de un Future, el bucle de eventos ejecuta otras
tareas, retorno de llamada o realiza operaciones de E/S.

Utilice la función de alto nivel asyncio.create_task() para crear Tareas, o las funciones de bajo
nivel loop.create_task() o ensure_future(). Se desaconseja la creación de instancias manuales de
Tareas.

Para cancelar una Tarea en ejecución, utilice el método cancel(). Llamarlo hará que la tarea lance
una excepción CancelledError en la corrutina contenida. Si una corrutina está esperando en un
objeto Future durante la cancelación, se cancelará el objeto Future.

cancelled() se puede utilizar para comprobar si la Tarea fue cancelada. El método retorna True si la
corrutina contenida no suprimió la excepción CancelledError y se canceló realmente.

asyncio.Task hereda de Future todas sus API excepto Future.set_result() y Future.set_exception().

Un argumento context opcional de solo palabra clave permite especificar


un contextvars.Context personalizado para que se ejecute coro. Si no se proporciona
ningún context, la tarea copia el contexto actual y luego ejecuta su rutina en el contexto copiado.

Un argumento eager_start opcional de solo palabra clave permite iniciar con entusiasmo la
ejecución de asyncio.Task en el momento de la creación de la tarea. Si se establece en True y el
bucle de eventos se está ejecutando, la tarea comenzará a ejecutar la corrutina inmediatamente,
hasta la primera vez que la corrutina se bloquee. Si la rutina regresa o se activa sin bloquearse, la
tarea finalizará con entusiasmo y saltará la programación al bucle de eventos.

Distinto en la versión 3.7: Agregado soporte para el módulo contextvars.

Distinto en la versión 3.8: Se ha añadido el parámetro name.

Obsoleto desde la versión 3.10: Se emite una advertencia de obsolescencia si no se


especifica loop y no hay un bucle de eventos en ejecución.

Distinto en la versión 3.11: Se ha añadido el parámetro context.

Distinto en la versión 3.12: Se ha añadido el parámetro eager_start.

done()

Retorna True si la Tarea está finalizada.


Una tarea está finalizada cuando la corrutina contenida retornó un valor, lanzó una excepción, o se
canceló la Tarea.

result()

Retorna el resultado de la Tarea.

Si la tarea está terminada, se retorna el resultado de la corrutina contenida (o si la corrutina lanzó


una excepción, esa excepción se vuelve a relanzar.)

Si la Tarea ha sido cancelada, este método lanza una excepción CancelledError.

If the Task’s result isn’t yet available, this method raises an InvalidStateError exception.

exception()

Retorna la excepción de la Tarea.

Si la corrutina contenida lanzó una excepción, esa excepción es retornada. Si la corrutina


contenida retorna normalmente, este método retorna None.

Si la Tarea ha sido cancelada, este método lanza una excepción CancelledError.

Si la Tarea aún no está terminada, este método lanza una excepción InvalidStateError.

add_done_callback(callback, *, context=None)

Agrega una retro llamada que se ejecutará cuando la Tarea esté terminada.

Este método solo se debe usar en código basado en retrollamada de bajo nivel.

Consulte la documentación de Future.add_done_callback() para obtener más detalles.

remove_done_callback(callback)

Remueve la retrollamada de la lista de retrollamadas.

Este método solo se debe usar en código basado en retrollamada de bajo nivel.

Consulte la documentación de Future.remove_done_callback() para obtener más detalles.

get_stack(*, limit=None)

Retorna la lista de marcos de pila para esta tarea.

Si la corrutina contenida no se termina, esto retorna la pila donde se suspende. Si la corrutina se ha


completado correctamente o se ha cancelado, retorna una lista vacía. Si la corrutina terminó por
una excepción, esto retorna la lista de marcos de seguimiento.

Los marcos siempre se ordenan de más antiguo a más nuevo.

Solo se retorna un marco de pila para una corrutina suspendida.

El argumento opcional limit establece el número máximo de marcos que se retornarán; de forma
predeterminada se retornan todos los marcos disponibles. El orden de la lista retornada varía en
función de si se retorna una pila o un traceback: se retornan los marcos más recientes de una pila,
pero se retornan los marcos más antiguos de un traceback. (Esto coincide con el comportamiento
del módulo traceback.)ss

print_stack(*, limit=None, file=None)

Imprime la pila o el seguimiento de esta tarea.

Esto produce una salida similar a la del módulo traceback para los marcos recuperados
por get_stack().

El argumento limit se pasa directamente a get_stack().

El argumento file es un flujo de E/S en el que se escribe la salida; por defecto, la salida se escribe
en sys.stdout.

get_coro()

Retorna el objeto corrutina contenido por Task.

Nota

Esto devolverá None para las tareas que ya se han completado con entusiasmo. Consulte el Eager
Task Factory.

Added in version 3.8.

Distinto en la versión 3.12: La ejecución ansiosa de la tarea recientemente agregada significa que el
resultado puede ser None.

get_context()

Devuelve el objeto contextvars.Context asociado con la tarea.

Added in version 3.12.

get_name()

Retorna el nombre de la Tarea.

Si no se ha asignado explícitamente ningún nombre a la Tarea, la implementación de Tarea asyncio


predeterminada genera un nombre predeterminado durante la creación de instancias.

Added in version 3.8.

set_name(value)

Establece el nombre de la Tarea.

El argumento value puede ser cualquier objeto, que luego se convierte en una cadena.
En la implementación de Task predeterminada, el nombre será visible en la salida repr() de un
objeto de tarea.

Added in version 3.8.

cancel(msg=None)

Solicita que se cancele la Tarea.

Esto hace que una excepción CancelledError sea lanzada a la corrutina contenida en el próximo
ciclo del bucle de eventos.

Luego, la corrutina tiene la oportunidad de limpiar o incluso denegar la solicitud suprimiendo la


excepción con un bloque try… … except CancelledError… finally. Por lo tanto, a diferencia
de Future.cancel(), Task.cancel() no garantiza que la tarea se cancelará, aunque suprimir la
cancelación por completo no es común y se desaconseja activamente. Sin embargo, si la rutina
decide suprimir la cancelación, debe llamar a Task.uncancel() además de detectar la excepción.

Distinto en la versión 3.9: Se agregó el parámetro msg.

Distinto en la versión 3.11: El parámetro msg se propaga desde la tarea cancelada a su espera.

En el ejemplo siguiente se muestra cómo las corrutinas pueden interceptar la solicitud de


cancelación:

async def cancel_me():

print('cancel_me(): before sleep')

try:

# Wait for 1 hour

await asyncio.sleep(3600)

except asyncio.CancelledError:

print('cancel_me(): cancel sleep')

raise

finally:

print('cancel_me(): after sleep')

async def main():

# Create a "cancel_me" Task

task = asyncio.create_task(cancel_me())
# Wait for 1 second

await asyncio.sleep(1)

task.cancel()

try:

await task

except asyncio.CancelledError:

print("main(): cancel_me is cancelled now")

asyncio.run(main())

# Expected output:

# cancel_me(): before sleep

# cancel_me(): cancel sleep

# cancel_me(): after sleep

# main(): cancel_me is cancelled now

cancelled()

Retorna True si la Tarea se cancela.

La tarea se cancela cuando se solicitó la cancelación con cancel() y la corrutina contenida propagó
la excepción CancelledError que se le ha lanzado.

uncancel()

Disminuye el recuento de solicitudes de cancelación a esta tarea.

Retorna el número restante de solicitudes de cancelación.

Tenga en cuenta que una vez que se completa la ejecución de una tarea cancelada, las llamadas
posteriores a uncancel() no son efectivas.

Added in version 3.11.

Este método lo usan los componentes internos de asyncio y no se espera que lo use el código del
usuario final. En particular, si una Tarea se cancela con éxito, esto permite que elementos de
concurrencia estructurada como Grupos de tareas y asyncio.timeout() continúen ejecutándose,
aislando la cancelación al bloque estructurado respectivo. Por ejemplo:

async def make_request_with_timeout():

try:

async with asyncio.timeout(1):

# Structured block affected by the timeout:

await make_request()

await make_another_request()

except TimeoutError:

log("There was a timeout")

# Outer code not affected by the timeout:

await unrelated_code()

Si bien el bloque con make_request() y make_another_request() podría cancelarse debido al tiempo


de espera, unrelated_code() debería continuar ejecutándose incluso en caso de que se agote el
tiempo de espera. Esto se implementa con uncancel(). Los administradores de
contexto TaskGroup usan uncancel() de manera similar.

If end-user code is, for some reason, suppressing cancellation by catching CancelledError, it needs
to call this method to remove the cancellation state.

When this method decrements the cancellation count to zero, the method checks if a
previous cancel() call had arranged for CancelledError to be thrown into the task. If it hasn’t been
thrown yet, that arrangement will be rescinded (by resetting the internal _must_cancel flag).

Distinto en la versión 3.13: Changed to rescind pending cancellation requests upon reaching zero.

cancelling()

Retorna el número de solicitudes de cancelación pendientes a esta Tarea, es decir, el número de


llamadas a cancel() menos el número de llamadas a uncancel().

Tenga en cuenta que si este número es mayor que cero pero la tarea aún se está
ejecutando, cancelled() aún retornará False. Esto se debe a que este número se puede reducir
llamando a uncancel(), lo que puede provocar que la tarea no se cancele después de todo si las
solicitudes de cancelación se reducen a cero.

Este método lo utilizan las partes internas de asyncio y no se espera que lo utilice el código del
usuario final. Consulte uncancel() para obtener más detalles.

Caching de Funciones
El caché es un término muy usado en informática, y hace referencia al almacenamiento de
resultados previos para su posterior reutilización, lo que permite reducir el tiempo de respuesta.
Por ejemplo, si llamamos a una función con un determinado parámetro y acto seguido realizamos
la misma llamada, sería interesante reutilizar el primer resultado para no tener que calcularlo otra
vez. Existen por lo tanto dos posibilidades:

• Si ejecutamos la función y el resultado no ha sido calculado con anterioridad, se calcula y se


almacena por si fuera útil en el futuro. Esto se conoce como cache miss.

• Si ejecutamos la función y el caché tiene almacenado el resultado para esa operación, en


vez de calcular otra vez la salida la podemos reutilizar, lo que se conoce como cache hit.
Dado que estamos reutilizando un valor ya calculado, generalmente el tiempo de respuesta
será menor.

Por suerte, Python nos permite añadir caching a nuestras funciones, pero antes de implementarlo
es conveniente hacer un análisis sobre nuestro programa y determinar si merece la pena. Algunas
cosas a tener en cuenta:

• El caching es especialmente útil cuando trabajamos con funciones muy intensivas en


cálculo, lo que hace que reutilizar el valor del caché reduzca notablemente el tiempo de
respuesta.

• Es necesario conocer (a nivel estadístico) la distribución de los argumentos con los que se
llama la función. Si la función bajo estudio se llama con valores muy dispares y apenas
repetidos, el caching poco ayudará, ya que apenas tendremos un cache hit.

• El uso de un caché puede mejorar el tiempo de respuesta, pero frecuentemente se paga en


un incremento del uso de memoria. También es necesario decidir el número de valores a
almacenar.

A continuación veremos como implementar caching en Python, pudiendo hacerlo


con diccionarios o utilizando la librería functools. Para ejemplificarlo, veremos como implementar
un caché en nuestro código de números primos visto anteriormente, empleando ambas formas.

def es_primo(num):

for n in range(2, num):

if num % n == 0:

return False

return True

Caching con Diccionarios

La primera forma de realizarlo es usando un diccionario como caché. Nótese que este es un
ejemplo didáctico, y que obvia algunos factores. Como puedes ver tenemos claramente
diferenciado el cache hit y el cache miss. Si el valor no está en el caché se calcula y se devuelve.
def es_primo_concache(num, _cache={}):

if num not in _cache:

_cache[num] = True

for n in range(2, num):

if num % n == 0:

_cache[num] = False

break

return _cache[num]

Dado que es_primo es bastante intensivo en cálculo, cuando usamos números grandes el ahorro
puede ser muy significativo. En el siguiente código podemos ver como la primera vez que
ejecutamos la función, se tardan 3.5 segundos, ya que el resultado tiene que ser calculado. Sin
embargo la segunda vez que la llamamos con la misma entrada, tenemos un cache hit, por lo que el
valor ya no es calculado sino recuperado del caché, tardando microsegundos.

import time

tic = time.time()

es_primo_concache(25565479)

print(time.time() - tic)

tic = time.time()

es_primo_concache(25565479)

print(time.time() - tic)

# 3.5551438331604004

# 4.0531158447265625e-06

Caching con functools y lru_cache

La segunda forma de realizarlo, y un poco más sofisticada es usando lru_cache, un decorador que
viene con la librería estándar functools. La mayor ventaja es que no necesitamos modificar la
función. Nótese que maxsize nos permite indicar el número máximo de valores que queremos
almacenar en el caché.

from functools import lru_cache


@lru_cache(maxsize=32)

def es_primo_concache(num):

for n in range(2, num):

if num % n == 0:

return False

return True

Por lo tanto si ahora llamamos a nuestra función con los mismos valores, podemos ver como la
primera vez tarda 3.9 segundos, pero la segunda apenas tarda unos microsegundos.

import time

tic = time.time()

es_primo_concache(25565479)

print(time.time() - tic)

tic = time.time()

es_primo_concache(25565479)

print(time.time() - tic)

# 3.9316678047180176

# 3.0994415283203125e-06

En el caso de que queramos limpiar el caché de nuestra función, podemos realizar lo siguiente.

es_primo_concache.cache_clear()

Programación Funcional en Python

En pocas palabras, la programación funcional es un paradigma de programación distinto al


tradicional estructurado u orientado a objetos al que solemos estar acostumbrados. Se basa
principalmente en el uso de funciones, siendo las mismas practicamente la única herramienta que
el lenguaje nos ofrece. Por ello, en lenguajes puramente funcionales como Haskell no existen
bucles for o while.

¿Un lenguaje de programación sin bucles? Aunque pueda parecer una locura, también tiene sus
ventajas, y ofrece ciertas características muy importantes que veremos a continuación.
A pesar de que Python no es un lenguaje puramente funcional, nos ofrece algunas primitivas
propias de lenguajes funcionales, como map, filter y reduce. Todas ellas ofrecen una alternativa al
uso de bucles para resolver ciertos problemas. Veamos unos ejemplos.

map en Python

La función map toma dos entradas:

• Una lista o iterable que será modificado en una nueva.

• Una función, que será aplicada a cada uno de los elementos de la lista o iterable anterior.

Nos devuelve una nueva lista donde todos y cada uno de los elementos de la lista original han sido
pasados por la función.

map(funcion_a_aplicar, entrada_iterable)

Imaginemos que queremos multiplicar por dos todos los elementos de una lista. La primera forma
que se nos podría ocurrir sería la siguiente. Nótese que también podría usarse list comprehension,
pero eso lo dejamos para otro artículo.

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

lista_pordos = []

for l in lista:

lista_pordos.append(l*2)

print(lista_pordos)

# [2, 4, 6, 8, 10]

Pero veamos como darle un enfoque funcional. Como hemos dicho, map toma una función y un
iterable como entrada, aplicando la función a cada elemento. Entonces podemos definir una
función por_dos, que pasaremos a map junto con nuestra lista inicial.

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

def por_dos(x):

return x * 2

lista_pordos = map(por_dos, lista)

print(list(lista_pordos))
# [2, 4, 6, 8, 10]

Como podemos observar, ya no usamos bucles. Simplemente le pasamos a map la función y la lista
y ya hace el trabajo por nosotros. Dado que por_dos se trata de una función muy sencilla, es posible
usar funciones lambda para compactar un poco el código, quedando lo siguiente:

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

lista_pordos = map(lambda x: 2*x, lista)

print(list(lista_pordos))

# [2, 4, 6, 8, 10]

filter en Python

La función filter también recibe una función y una lista pero el resultado es la lista inicial filtrada.
Es decir, se pasa cada elemento de la lista por la función, y sólo si su resultado es True, se incluye
en la nueva lista.

filter(funcion_filtrar, entrada_iterable)

Al igual que hacíamos antes, usamos las funciones lambda que nos permiten declarar y asignar
una función en la misma línea de código. En el siguiente ejemplo filtramos los números pares.

lista = [7, 4, 16, 3, 8]

pares = filter(lambda x: x % 2 == 0, lista)

print(list(pares))

# [4, 16, 8]

Nótese que el siguiente código sería equivalente:

lista = [7, 4, 16, 3, 8]

def es_par(x):

return x % 2 == 0

pares = filter(es_par, lista)

print(list(pares))
# [4, 16, 8]

Una vez más, resaltar que no estamos usando bucles. Simplemente damos la entrada y la función a
aplicar a cada elemento, y filter se encarga del resto. Esta es una de las características clave de la
programación funcional. Se centra más en el qué hacer que en el cómo hacerlo. Para ello se usan
funciones predefinidas como las que estamos viendo, a las que sólo tenemos que pasar las
entradas y hacer el trabajo por nosotros.

reduce en Python

Por último, podemos usar reduce para reducir todos los elementos de la entrada a un único
valor aplicando un determinado criterio. Por ejemplo, podemos sumar todos los elementos de una
lista de la siguiente manera.

from functools import reduce

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

suma = reduce(lambda acc, val: acc + val, lista)

print(suma) # 15

Lo que podría reescribirse usando la función add:

from operator import add

from functools import reduce

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

suma = reduce(add, lista)

print(suma) # 15

O también los podemos multiplicar, modificando la función lambda.

from functools import reduce

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

multiplicacion = reduce(lambda acc, val: acc * val, lista)

print(multiplicacion) # 120

Es importante notar que la función recibe dos argumentos: el acumulador y cada uno de los valores
de la lista.

• El acumulador es el valor devuelto en la iteración anterior, que va acumulando un resultado


hasta que llegamos al final.

• El valor es cada uno de los elementos de nuestra lista, que en nuestro caso vamos
añadiendo al acumulador.
El uso de reduce es especialmente útil cuando tenemos por ejemplo una lista de diccionarios y
queremos sumar todos los valores de un campo en concreto. Veamos un ejemplo donde
calculamos la edad media de varias personas.

from functools import reduce

personas = [

{'Nombre': 'Alicia', 'Edad': 22},

{'Nombre': 'Bob', 'Edad': 29},

{'Nombre': 'Charlie', 'Edad': 33}

suma_edad = reduce(lambda total, p: total + p['Edad'], personas, 0)

print(suma_edad/len(personas)) # 28.0

Nótese que llamamos a reduce con un valor adicional 0, que es el valor inicial del acumulador. Una
vez más, hemos resuelto un problema en el que tradicionalmente usaríamos bucles con las
primitivas de la programación funcional.

Características de la Programación Funcional

Una vez entendido el funcionamiento de map, filter y reduce, ya tenemos unas nociones básicas de
la programación funcional. Sus características más importantes son las siguientes:

• Los lenguajes de programación puramente funcionales carecen de bucles for y while.

• Se dice que en la programación funcional, las funciones son “ciudadanas de primera clase”,
lo que nos viene a decir que prácticamente todo se hace con funciones, y no con bucles.

• La programación funcional prefiere también las funciones puras, es decir, funciones sin side
effects. Las funciones puras no dependen de variables externas o globales. Esto significa
que para las mismas entradas, siempre se producen las mismas salidas.

• Por otro lado, en la programación funcional se prefiere variables inmutables, lo que significa
que una vez creadas no pueden ser modificadas. Esto es algo que verdaderamente ahorra
problemas, aunque la eficiencia puede ser discutible.

• En general, se considera que los lenguajes de programación funcionales son más seguros, y
ofrecen formal verification.

Por último, resaltar una vez más que aunque Python no es un lenguaje puramente funcional, ofrece
algunas funciones propias de lenguajes funcionales como map, filter y reduce. Si estás interesado
en más, puedes echar un vistazo a el paquete itertools.

Ejemplos Programación Funcional

Dada una lista de personas almacenadas en diccionarios:


• Filtrar de acuerdo al sexo

• Y calcular la media

from functools import reduce

personas = [

{'Nombre': 'Alicia', 'Edad': 22, 'Sexo': 'F'},

{'Nombre': 'Bob', 'Edad': 25, 'Sexo': 'M'},

{'Nombre': 'Charlie', 'Edad': 33, 'Sexo': 'M'},

{'Nombre': 'Diana', 'Edad': 15, 'Sexo': 'F'},

{'Nombre': 'Esteban', 'Edad': 30, 'Sexo': 'M'},

{'Nombre': 'Federico', 'Edad': 44, 'Sexo': 'M'},

hombres = list(filter(lambda x: x['Sexo'] == 'M', personas))

suma_edades = reduce(lambda suma, p: suma + p['Edad'], hombres, 0)

media_edad = suma_edades/(len(hombres))

print(media_edad) # 33.0

Tal vez no muy legible, pero todo lo anterior se podrá expresar en una única línea de código.

media_edades = reduce(lambda suma, p: suma + p['Edad'], filter(lambda x: x['Sexo'] == 'M',


personas), 0) / len(list(filter(lambda x: x['Sexo'] == 'M', personas)))

print(media_edades) # 33.0

OPERADORES

• Operadores de asignación

• 📗📗 Operadores Aritméticos

• 📗📗 Operadores Relacionales

• 📗📗 Operadores Lógicos

• 📙📙 Operadores Bitwise

• 📙📙 Operadores Identidad

• 📙📙 Operadores Membresía
• 📙📙 Operador walrus

Operadores de asignación
Anteriormente hemos visto los operadores aritméticos, que usaban dos números para calcular una
operación aritmética (como suma o resta) y devolver su resultado. En este caso, los operadores de
asignación o assignment operators nos permiten realizar una operación y almacenar su resultado
en la variable inicial. Podemos ver como realmente el único operador nuevo es el = . El resto son
abreviaciones de otros operadores que habíamos visto con anterioridad. Ponemos un ejemplo
con x=7

Operador Ejemplo Equivalente

= x=7 x=7

+= x+=2 x=x+2 = 7

-= x-=2 x=x-2 = 5

*= x*=2 x=x*2 = 14

/= x/=2 x=x/2 = 3.5

%= x%=2 x=x%2 = 1

//= x//=2 x=x//2 = 3

**= x**=2 x=x**2 = 49

&= x&=2 x=x&2 = 2

|= x|=2 x=x|2 = 7

^= x^=2 x=x^2 = 5

>>= x>>=2 x=x>>2 = 1

&lt&lt= x&lt&lt=2 x=x&lt&lt2 = 28

a=7; b=2
print("Operadores de asignación")
x=a; x+=b; print("x+=", x) # 9
x=a; x-=b; print("x-=", x) # 5
x=a; x*=b; print("x*=", x) # 14
x=a; x/=b; print("x/=", x) # 3.5
x=a; x%=b; print("x%=", x) # 1
x=a; x//=b; print("x//=", x) # 3
x=a; x**=b; print("x**=", x) # 49
x=a; x&=b; print("x&=", x) # 2
x=a; x|=b; print("x|=", x) # 7
x=a; x^=b; print("x^=", x) # 5
x=a; x>>=b; print("x>>=", x) # 1
x=a; x<<=b; print("x<<=", x) # 28

Operador =
El operador = prácticamente no necesita explicación, simplemente asigna a la variable de la
izquierda el contenido que le ponemos a la derecha. Ponemos en negrita variable porque si
hacemos algo del tipo 3=5 tendremos un error. Como siempre, nunca te fíes de nada y experimenta
con ello.
x=2 # Uso correcto del operador =
print(x) # 2
#3=5 # Daría error, 3 no es una variable

Tal vez pienses que el operador = es trivial y apenas merezca explicación, pero es importante
explorar los límites del lenguaje. Si sabes lo que es un puntero, o una referencia tal vez el ejemplo
siguiente tenga sentido para tí. Vamos a ver, si todo lo que hemos visto anteriormente es
cierto, a=[1, 2, 3] asigna [1, 2, 3] a a , por lo que si no tocamos a , el valor de a deberá ser
siempre [1, 2, 3] . Bueno, pues en el siguiente ejemplo vemos como eso no es así, el valor de a ha
cambiado. Se podría decir que no es lo mismo x=3 con un número que x=[1, 2, 3] con una lista.
No te preocupes si no lo has seguido, en otros capítulos lo explicaremos mejor.
a = [1, 2, 3]
b = a
b += [4]
print(a)
# [1, 2, 3, 4]

Operador +=
Como podemos ver, todos los operadores de asignación no son más que atajos para escribir otros
operadores de manera más corta, y asignar su resultado a la variable inicial. El
operador += en x+=1 es equivalente a x=x+1 . Sabiendo esto, sería justo preguntarse ¿realmente
merece la pena crear un operador nuevo que hace algo que ya podemos hacer pero de manera mas
corta? Bien, la pregunta no es fácil de responder y en cierto modo viene heredado de lenguajes
como C que en los años 1970s introdujeron esto.
x=5 # Ejemplo de como incrementar
x+=1 # en una unidad x
print(x)
# 6

Para saber más: Aunque se podría decir que el operador x+=1 es igual que x=x+1, no es del todo
cierto. De hecho el operador que Python invoca por debajo es "__iadd__" en el primer caso frente
a "__add__" para el segundo. A efectos prácticos, se podría considerar lo mismo, pero aquí puedes
leer más sobre esto

Se puede jugar un poco con el operador += y aplicarlo sobre variables que no necesariamente son
números. Como vimos en otros capítulos, se podría emplear sobre una lista.
x=[1,2,3] # En este caso la x es una lista
x+=[4,5] # Se aplica el operador sobre otra lista
print(x) # Y el resultado de la unión de ambas
# [1, 2, 3, 5, 6]

Es muy importante, que si x es una lista, no podemos aplicar el operador += con un elemento que
no sea una lista, como por ejemplo, un número. El siguiente código daría error porque el operador
no esta definido para un elemento lista y otro entero.
x=[1,2,3] #
#x+=3 # ERROR! TypeError

Operador -=
El operador -= es equivalente a restar y asignar el resultado a la variable inicial. Es decir, x-=1 es
equivalente a x=x-1 . Si vienes de otros lenguajes de programación, tal vez conozcas la forma x-- ,
pero en Python no existe. El operador es muy usado para decrementar el valor de una variable.
i = 5
i -= 1
print(i) # 4

Y algo que nunca se haría en la realidad, pero nos permite explorar los límites del lenguaje, sería
restar -1, lo que equivale a sumar uno. Pero de verdad, no hagas esto, en serio.
i = 0
i-=-1 # Aumenta el contador
print(i) # 1

Operador *=
El operador *= equivale a multiplicar una variable por otra y almacenar el resultado en la primera,
es decir x*=2 equivale a x=x*2 . Hasta ahora hemos usado todos los operadores de asignación con
una variable y un número, pero es totalmente correcto hacerlo con dos variables.
a=10; b=2 # Inicializamos a 10 y 20
a*=b # Usando dos variables
print(a) # 20

Operador /=
El operador /= equivale a dividir una variable por otra y almacenar el resultado en la primera, es
decir, x/=2 equivale a x=x/2 . Acuérdate que en otros capítulos vimos como 5/3 en versiones
antiguas de Python, podía causar problemas ya que el resultado no era un numero entero. En el
siguiente ejemplo podemos ver como Python hace el trabajo por nosotros, y cambia el tipo de la
variable x de lo que inicialmente era int a un float con el objetivo de que el nuevo valor pueda
ser almacenado.
x = 10
print(type(x)) # <class 'int'>
x/=3
print(type(x)) # <class 'float'>

Operador %=
El operador %= equivale a hacer el módulo de la división de dos variables y almacenar su resultado
en la primera.
x = 3
x%=2
print(x) # 1

Una curiosidad a tener en cuenta, es que el operador módulo tiene diferentes comportamientos en
Python del que tiene en otros lenguajes como C cuando se usan números negativos tanto de
dividendo como de divisor. Así que ten cuidado si haces cosas como las siguientes.
print(-5%-3) # -2
print(5%-3) # -1
print(-5%3) # 1
print(5%3) # 2

Operador //=
El operador //= realiza la operación cociente entre dos variables y almacena el resultado en la
primera. El equivalente de x//=2 sería x=x//2 .
x=5 # El resultado es el cociente
x//=3 # de la división
print(x) # 1

Operador **=
El operador **= realiza la operación exponente del primer número elevado al segundo, y almacena
el resultado en la primera variable. El equivalente de x**=2 sería x=x**2 .
x=5 # Eleva el número al cuadrado
x**=2 # y guarda el resultado en la misma
print(x) # 25
Otro ejemplo similar, sería empleando un exponente negativo, algo que es totalmente válido y
equivale matemáticamente al inverso del número elevado al exponente en positivo. Dicho de otra
forma, x−2 equivale a 1/x2.
x=5 # Elevar 5 a -2 equivale a dividir
x**=-2 # uno entre 25.
print(x) # 0.04

Operador &=
El operador &= realiza la comparación & bit a bit entre dos variables y almacena su resultado en la
primera. El equivalente de x&=1 sería x=x&1
a = 0b101010
a&= 0b111111
print(bin(a))
# 0b101010

Operador |=
El operador |= realiza el operador | elemento a elemento entre dos variables y almacena su
resultado en la primera. El equivalente de x|=2 sería x=x|2
a = 0b101010
a|= 0b111111
print(bin(a))
# 0b111111

Operador ^=
El operador ^= realiza el operador ^ elemento a elemento entre dos variables y almacena su
resultado en la primera. El equivalente de x^=2 sería x=x^2
a = 0b101010
a^= 0b111111
print(bin(a))
# 0b10101

Operador »=
El operador >>= es similar al operador >> pero permite almacenar el resultado en la primera
variable. Por lo tanto x>>=3 sería equivalente a x=x>>3
x = 10
x>>=1
print(x) # 5

Es importante tener cuidado y saber el tipo de la variable x antes de aplicar este operador, ya que
se podría dar el caso de que x fuera una variable tipo float . En ese caso, tendríamos un error
porque el operador >> no esta definido para float .
x=10.0 # Si la x es float
print(type(x)) # <class 'float'>
#x>>=1 # ERROR! TypeError

Operador «=
Muy similar al anterior, <<= aplica el operador << y almacena su contenido en la primera variable.
El equivalente de x<<=1 sería x=x<<1
x=10 # Inicializamos a 10
x<<=1 # Desplazamos 1 a la izquierda
print(x) # 20
Sería justo pensar que si << realiza un desplazamiento de bits a la izquierda y >> lo realiza a la
derecha, tal vez un desplazamiento << una unidad, podría equivaler a -1 desplazamiento a la
derecha.
#x<<=-1 # ERROR! Python no define un desplazamiento negativo a la izquierda
#x>>=-1 # ERROR! Python no define un desplazamiento negativo a la derecha
Operadores aritméticos

Los operadores aritméticos o arithmetic operators son los más comunes que nos podemos
encontrar, y nos permiten realizar operaciones aritméticas sencillas, como pueden ser la suma,
resta o exponente. A continuación, condensamos en la siguiente tabla todos ellos con un ejemplo,
donde x=10 y y=3.

Operador Nombre Ejemplo

+ Suma x + y = 13

- Resta x-y=7

* Multiplicación x * y = 30

/ División x/y = 3.333

% Módulo x%y = 1

** Exponente x ** y = 1000

// Cociente 3

x = 10; y = 3

print("Operadores aritméticos")

print("x+y =", x+y) #13

print("x-y =", x-y) #7

print("x*y =", x*y) #30

print("x/y =", x/y) #3.3333333333333335

print("x%y =", x%y) #1

print("x**y =", x**y) #1000

print("x//y =", x//y) #3

Operador +

El operador + suma los números presentes a la izquierda y derecha del operador. Recalcamos lo de
números porque no tendría sentido sumar dos cadenas de texto, o dos listas, pero en Python es
posible hacer este tipo de cosas.
print(10 + 3) # 13

Es posible sumar también dos cadenas de texto, pero la suma no será aritmética, sino que se
unirán ambas cadenas en una. También se pueden sumar dos listas, cuyo resultado es la unión de
ambas.

print("2" + "2") # 22

print([1, 3] + [6, 7]) # [1, 3, 6, 7]

Operador -

El operador - resta los números presentes a la izquierda y derecha del operador. A diferencia el
operador + en este caso no podemos restar cadenas o listas.

print(10 - 3) #7

Operador *

El operador * multiplica los números presentes a la izquierda y derecha del operador.

print(10 * 3) #30

Como también pasaba con el operador + podemos hacer cosas “raras” con *. Explicar porque pasan
estas cosas es un poquito más complejo, por lo que lo dejamos para otro capítulo, donde
explicaremos como definir el comportamiento de determinados operadores para nuestras clases.

print("Hola" * 3) #HolaHolaHola

Operador /

El operador / divide los números presentes a la izquierda y derecha del operador. Un aspecto
importante a tener en cuenta es que si realizamos una división cuyo resultado no es entero (es
decimal) podríamos tener problemas. En Python 3 esto no supone un problema porque el mismo se
encarga de convertir los números y el resultado que se muestra si es decimal.

print(10/3) #3.3333333333333335

print(1/2) #0.5

Sin embargo, en Python 2, esto hubiera tenido un resultado diferente. El primer ejemplo 10/3=3 y el
segundo 1/2=0. El comportamiento realmente sería el de calcular el cociente y no la división.

Para saber más: Si quieres saber más acerca de este cambio del operador de división, puedes leer
la la PEP238

Operador %

El operador % realiza la operación módulo entre los números presentes a la izquierda y la derecha.
Se trata de calcular el resto de la división entera entre ambos números. Es decir, si dividimos 10
entre 3, el cociente sería 3 y el resto 1. Ese resto es lo que calcula el módulo.

print(10%3) # 1
print(10%2) # 0

Operador **

El operador ** realiza el exponente del número a la izquierda elevado al número de la derecha.

print(10**3) #1000

print(2**2) #4

Si ya has usado alguna vez Python, tal vez hayas oido hablar de la librería math. En esta librería
también tenemos una función llamada pow() que es equivalente al operador **.

import math

print(math.pow(10, 3)) #1000.0

Para saber más: Puedes consultar más información de la librería math en la documentación oficial
de Python

Operador //

Por último, el operador // calcula el cociente de la división entre los números que están a su
izquierda y derecha.

print(10//3) #3

print(10//10) #1

Tal vez te hayas dado cuenta que el operador cociente // está muy relacionado con el operador
módulo %. Volviendo a las lecciones del colegio sobre la división, recordaremos que el
Dividendo D es igual al divisor d multiplicado por el cociente c y sumado al resto r, es decir D=d*c+r.
Se puede ver como en el siguiente ejemplo, 10//3 es el cociente y 10%3 es el resto. Al aplicar la
fórmula, verificamos que efectivamente 10 era el dividendo.

D = 10 # Número que queremos dividir

d = 3 # Número entre el que queremos dividir

print(3 * (10//3) + 10%3) # 10

Orden de aplicación

En los ejemplos anteriores simplemente hemos aplicado un operador a dos números sin
mezclarlos entre ellos. También es posible tener varios operadores en la misma línea de código, y
en este caso es muy importante tener en cuenta las prioridades de cada operador y cual se aplica
primero. Ante la duda siempre podemos usar paréntesis, ya que todo lo que está dentro de un
paréntesis se evaluará conjuntamente, pero es importante saber las prioridades.

El orden de prioridad sería el siguiente para los operadores aritméticos, siendo el primero el de
mayor prioridad:

• () Paréntesis
• ** Exponente

• -x Negación

• * / // Multiplicación, División, Cociente, Módulo

• + - Suma, Resta

print(10*(5+3)) # Con paréntesis se realiza primero la suma

# 80

print(10*5+3) # Sin paréntesis se realiza primero la multiplicación

# 53

print(3*3+2/5+5%4) # Primero se multiplica y divide, después se suma

#10.4

print(-2**4) # Primero se hace la potencia, después se aplica el signo

#-16

Para saber más: Si quieres saber más sobre el orden de prioridad de diferentes operadores, aquí
tienes la documentación oficial de Python

Operadores relacionales

Los operadores relacionales, o también llamados comparison operators nos permiten saber
la relación existente entre dos variables. Se usan para saber si por ejemplo un número es mayor o
menor que otro. Dado que estos operadores indican si se cumple o no una operación, el valor que
devuelven es True o False. Veamos un ejemplo con x=2 e y=3

Operador Nombre Ejemplo

== Igual x == y = False

!= Distinto x != y = True

> Mayor x > y = False

&lt Menor x &lt y = True

>= Mayor o igual x >= y = False

&lt= Menor o igual x &lt y = True

x=2; y=3

print("Operadores Relacionales")

print("x==y =", x==y) # False


print("x!=y =", x!=y) # True

print("x>y =", x>y) # False

print("x<y =", x<y) # True

print("x>=y =", x>=y) # False

print("x<=y =", x<=y) # True

Operador ==

El operador == permite comparar si las variables introducidas a su izquierda y derecha son iguales.
Muy importante no confundir con =, que es el operador de asignación.

print(4==4) # True

print(4==5) # False

print(4==4.0) # True

print(0==False) # True

print("asd"=="asd") # True

print("asd"=="asdf") # False

print(2=="2") # False

print([1, 2, 3] == [1, 2, 3]) # True

Operador !=

El operador != devuelve True si los elementos a comparar son iguales y False si estos son distintos.
De hecho, una vez definido el operador ==, no sería necesario ni explicar != ya que hace
exactamente lo contrario. Definido primero, definido el segundo. Es decir, si probamos con los
mismos ejemplo que el apartado anterior, veremos como el resultado es el contrario, es
decir False donde era True y viceversa.

print(4!=4) # False

print(4!=5) # True

print(4!=4.0) # False

print(0!=False) # False

print("asd"!="asd") # False

print("asd"!="asdf") # True

print(2!="2") # True

print([1, 2, 3] != [1, 2, 3]) # False


Operador >

El operador > devuelve True si el primer valor es mayor que el segundo y False de lo contrario.

print(5>3) # True

print(5>5) # False

Algo bastante curioso, es como Python trata al tipo booleano. Por ejemplo, podemos ver
como True es igual a 1, por lo que podemos comprar el tipo True como si de un número se tratase.

print(True==1) # True

print(True>0.999) # True

Para saber más: De hecho, el tipo bool en Python hereda de la clase int. Si quieres saber más
acerca del tipo bool en Python puedes leer la PEP285

También se pueden comparar listas. Si los elementos de la lista son numéricos, se comparará
elemento a elemento.

print([1, 2] > [10, 10]) # False

Operador <

El operador < devuelve True si el primer elemento es mayor que el segundo. Es totalmente válido
aplicar operadores relacionales como < sobre cadenas de texto, pero el comportamiento es un
tanto difícil de ver a simple vista. Por ejemplo abc es menor que abd y A es menor que a

print("abc" < "abd") # True

print("A"<"a") # True

Para el caso de A y a la explicación es muy sencilla, ya que Python lo que en realidad está
comparando es el valor entero Unicode que representa tal caracter. La función ord() nos da ese
valor. Por lo tanto cuando hacemos "A"<"a" lo que en realidad hacemos es comprar dos números.

print(ord('A')) # 65

print(ord('a')) # 97

Para saber más: En el siguiente enlace tienes más información de como Python compara variables
que no son números.

Operador >=

Similar a los anteriores, >= permite comparar si el primer elemento es mayor o igual que es
segundo, devolviendo True en el caso de ser cierto.

print(3>=3) # True

print([3,4] >= [3,5]) # False

Operdor <=
De la misma manera, <= devuelve True si el primer elemento es menor o igual que el segundo. Nos
podemos encontrar con cosas interesantes debido a la precisión numérica existente al representar
valores, como el siguiente ejemplo.

print(3<=2.99999999999999999)

Operadores lógicos

Los operadores lógicos o logical operators nos permiten trabajar con valores de tipo booleano. Un
valor booleano o bool es un tipo que solo puede tomar valores True o False. Por lo tanto, estos
operadores nos permiten realizar diferentes operaciones con estos tipos, y su resultado será otro
booleano. Por ejemplo, True and True usa el operador and, y su resultado será True. A continuación
lo explicaremos mas en detalle.

Operador Nombre Ejemplo

and Devuelve True si ambos elementos son True True and True = True

or Devuelve True si al menos un elemento es True True or False = True

not Devuelve el contrario, True si es Falso y viceversa not True = False

Operador and

El operador and evalúa si el valor a la izquierda y el de la derecha son True, y en el caso de ser
cierto, devuelve True. Si uno de los dos valores es False, el resultado será False. Es realmente un
operador muy lógico e intuitivo que incluso usamos en la vida real. Si hace sol y es fin de semana,
iré a la playa. Si ambas condiciones se cumplen, es decir que la variable haceSol=True y la
variable finDeSemana=True, iré a la playa, o visto de otra forma irALaPlaya=(haceSol and
finDeSemana).

print(True and True) # True

print(True and False) # False

print(False and True) # False

print(False and False) # False

Operador or

El operador or devuelve True cuando al menos uno de los elementos es igual a True. Es decir, evalúa
si el valor a la izquierda o el de la derecha son True.

print(True or True) # True

print(True or False) # True

print(False or True) # True

print(False or False) # False


Es importante notar que varios operadores pueden ser usados conjuntamente, y salvo que existan
paréntesis que indiquen una cierta prioridad, el primer operador que se evaluará será el and. En el
ejemplo que se muestra a continuación, podemos ver que tenemos dos operadores and. Para
calcular el resultado final, tenemos que empezar por el and calculando el resultado en grupos de
dos y arrastrando el resultado hacia el siguiente grupo. El resultado del primer grupo sería True ya
que estamos evaluando True and True. Después, nos guardamos ese True y vamos a por el siguiente
y último elemento, que es False. Por lo tanto hacemos True and False por lo que el resultado final
es False

print(True and True and False)

# |-----------|

# True and False

# |------------------|

# False

También podemos mezclar los operadores. En el siguiente ejemplo empezamos por False and
True que es False, después False or True que es True y por último True or False que es True. Es decir,
el resultado final de toda esa expresión es True.

print(False and True or True or False)

# False and True = False

# Fase or True = True

# True or False = True

# True

Operador not

Y por último tenemos el operador not, que simplemente invierte True por False y False por True.
También puedes usar varios not juntos y simplemente se irán aplicando uno tras otro. La verdad
que es algo difícil de ver en la realidad, pero simplemente puedes contar el número de not y si es
par el valor se quedará igual. Si por lo contrario es impar, el valor se invertirá.

print(not True) # False

print(not False) # True

print(not not not not True) # True

Dado que estamos tratando con booleanos, hemos considerado que usar True y False es lo mejor y
más claro, pero es totalmente válido emplear 1 y 0 respectivamente para representar ambos
estados. Y por supuesto los resultados no varían

print(not 0) # True
print(not 1) # False

Ejemplos

Antes de entrar con los ejemplos, es importante notar que el orden de aplicación de los operadores
puede influir en el resultado, por lo que es importante tener muy claro su prioridad de aplicación.
De mayor a menor prioridad, el primer sería not, seguido de and y or.

Para saber más: Puedes leer la especificación oficial si quieres saber el orden de aplicación de
todos los operadores en este enlace

Pero, imaginemos que no sabemos el orden de aplicación de los operadores. Vamos a intentar con
ingeniería inversa descubrirlo con unos cuantos ejemplos. En el ejemplo que se muestra a
continuación, habría dos posibilidades. La primera sería (False and False) or True cuyo resultado
sería True y la segunda False and (False or True) cuyo resultado sería False. Dado que el resultado
que Python da es True, podemos concluir que el equivalente es la primera opción, es decir
que and tiene prioridad sobre el or

print(False and False or True)

# True

Lo mismo con el siguiente ejemplo. Como ya sabemos de antes que and es evaluado primero, la
siguiente expresión sería en realidad equivalente a True or (False and False) por lo que su resultado
es True

print(True or False and False)

# True

Como ya hemos indicado anteriormente, se puede usar 0 y 1 para


representar False y True respectivamente. Veamos a usar esa representación para abreviar.
Podemos simplificar la siguiente expresión, ya que el not es el operador que primer se aplica. Por lo
tanto nos quedaría 0 and 0 or 1 and 1 or 1 and 0. Y como ya sabemos, después se aplica el and, por
lo que nos quedaría (0 and 0) or (1 and 1) or (1 and 0). Y ya nos quedaría sólo aplicar el or de la
expresión resultante 0 or 1 or 1, por lo que el resultando final es 1 o True. Date cuenta que la
expresión se podría simplificar, y una vez en un or nos encontramos que un lado es True, podríamos
dejar de calcular el resto de la expresión ya que el resultado ya sería True

print(0 and not 1 or 1 and not 0 or 1 and 0)

# True

Operadores bitwise

Los operadores a nivel de bit o bitwise operators son operadores que actúan sobre números
enteros pero usando su representación binaria. Si aún no sabes como se representa un número en
forma binaria, a continuación lo explicamos.
Operador Nombre

| Or bit a bit

& And bit a bit

~ Not bit a bit

^ Xor bit a bit

>> Desplazamiento a la derecha

&lt&lt Desplazamiento a la izquierda

Para entender los operadores de bit, es importante antes entender como se representa un número
de manera binaria. Todos estamos acostumbrados a lo que se denomina representación decimal.
Se llama así porque se usan diez números, del 0 al 9 para representar todos los valores. Nuestro
decimal, es también posicional, ya que no es lo mismo un 9 en 19 que en 98. En el primer caso, el
valor es de simplemente 9, pero en el segundo, en realidad ese 9 vale 90.

Lo mismo pasa con la representación binaria pero con algunas diferencias. La primera diferencia es
que sólo existen dos posibles números, el 0 y el 1. La segunda diferencia es que a pesar de ser un
sistema que también es posicional, los valores no son como en el caso decimal, donde el valor se
multiplica por 1 en la primera posición, 10 en la segunda, 100 en la tercera, y así sucesivamente. En
el caso binario, los valores son potencias de 2, es decir 1, 2, 4, 8, 16 o lo que es lo mismo $$2^0, 2^1,
2^2, 2^3, 2^4$$

# Sistema decimal

# 7264

# 7-> En realidad vale 7*1000 = 7000

# 2-> En relidad vale 2*100 = 200

# 6-> En reaidad vale 6*10 = 60

# 4 En realidad vale 4*1 = 4

# +---------

# Sumando todo: 7264

Entonces por ejemplo el número en binario 11011 es en realidad el número 27 en decimal. Es


posible convertir entre binario y decimal y viceversa. Para números pequeños se puede hacer
mentalmente muy rápido, pero para números más grandes, os recomendamos hacer uso de alguna
función en Python, como la función bin()

# Sistema binario

# 11011
# 1-> En realidad vale 1*16 = 16

# 1-> En realidad vale 1*8 = 8

# 0-> En realidad vale 1*4 = 0

# 1-> En realidad vale 1*2 = 2

# 1-> En realidad vle 1*1 = 1

# +---------

# Sumando todo 27

Usando la función bin() podemos convertir un número decimal en binario. Podemos comprobar
como el número anterior 11011 es efectivamente 27 en decimal. Fíjate que al imprimirlo
con Python, se añade el prefijo 0b antes del número. Esto es muy importante, y nos permite
identificar que estamos ante un número binario.

print(bin(27))

# 0b11011

Ahora que ya sabemos como es la representación binaria, estamos ya en condiciones de continuar


con los operadores a nivel de bit, que realizan operaciones sobre los bits de estos números binarios
que acabamos de introducir.

Operador &

El operador & realiza la operación que vimos en otros capítulos and, pero por cada bit existente en
la representación binaria de los dos números que le introducimos. Es decir, recorre ambos
números en su representación binaria elemento a elemento, y hace una operación and con cada
uno de ellos. En el siguiente ejemplo se estaría haciendo lo siguiente. Primer elemento de a con
primer elemento de b, sería 1 and 1 por lo que el resultado es 1. Ese valor se almacena en la salida.
Continuamos con el segundo 1 and 0 que es 0, tercero 0 and 1 que es 0 y por último 1 and 1 que es 1.
Por lo tanto el número resultante es 0b1001, lo que representa el 9 en decimal.

a = 0b1101

b = 0b1011

print(bin(a & b))

#0b1001

Operador |

El operador | realiza la operación or elemento a elemento con cada uno de los bits de los números
que introducimos. En el siguiente ejemplo podemos ver como el resultado es 1111 ya que siempre
uno de los dos elementos es 1. Se harían las siguientes comparaciones 1 or 1, 1 or 0, 0 or 1 y 1 or 1.

a = 0b1101
b = 0b1011

print(bin(a | b))

# 0b1111

Operador ~

El operador ~ realiza la operación not sobre cada bit del número que le introducimos, es decir,
invierte el valor de cada bit, poniendo los 0 a 1 y los 1 a 0. El comportamiento en Python puede ser
algo distinto del esperado. En el siguiente ejemplo, tenemos el número 40 que en binario es 101000.
Si hacemos ~101000 sería de esperar que como hemos dicho, se inviertan todos los bits y el
resultado sea 010111, pero en realidad el resultado es 101001. Para entender porque pasa esto, te
invitamos a leer más información sobre el complemento a uno y el complemento a dos.

a = 40

print(bin(a))

print(bin(~a))

0b101000

-0b101001

Para saber más: Para entender este operador mejor, es necesario saber que es el complemento a
uno y a dos. Te dejamos este enlace con mas información.

Si vemos el resultado con números decimales, es equivalente a hacer ~a sería -a-1 como se puede
ver en el siguiente ejemplo. En este caso, en vez de mostrar el valor binario mostramos el decimal,
y se puede ver como efectivamente si a=40, tras aplicar el operador ~ el resultado es -40-1.

a = 40

print(a)

print(~a)

Operador ^

El operador ^ realiza la función xor con cada bit de las dos variables que se le proporciona.
Anteriormente en otros capítulos hemos hablado de la and o or, que son operadores bastante
usados y comunes. Tal vez xor sea menos común, y lo que hace es devolver True o 1 cuando hay al
menos un valor True pero no los dos. Es decir, solo devuelve 1 para las
combinaciones 1,0 y 0,1 y 0 para las demás.

x = 0b0110 ^ 0b1010

print(bin(x))

# 0 xor 1 = 1

# 1 xor 0 = 1
# 1 xor 1 = 0

# 0 xor 0 = 0

# 0b1100

Para saber más: Si quieres saber más sobre la puerta XOR, te dejamos un enlace donde se explica.

Operador >>

El operador >> desplaza todos los bit n unidades a la derecha. Por lo tanto es necesario
proporcionar dos parámetros, donde el primer es el número que se desplazará o shift y el segundo
es el número de posiciones. En el siguiente ejemplo tenemos 1000 en binario, por lo que si
aplicamos un >>2, deberemos mover cada bit 2 unidades a la derecha. Lo que queda por la
izquierda se rellena con ceros, y lo que sale por la derecha se descarta. Por lo
tanto 1000>>2 será 0010. Es importante notar que Python por defecto elimina los ceros a la
izquierda, ya que igual que en el sistema decimal, son irrelevantes.

a=0b1000

print(bin(a>>2))

# 0b10

Operador <<

El operador << es análogo al >> con la diferencia que en este caso el desplazamiento es realizado a
la izquierda. Por lo tanto si tenemos 0001 y desplazamos <<3, el resultado será 1000.

a=0b0001

print(bin(a<<3))

# 0b1000

Es importante que no nos dejemos engañar por el número de bits que ponemos a la entrada. Si por
ejemplo desplazamos en 4 o en mas unidades nuestra variable a el número de bits que se nos
mostrará también se incrementará. Con esto queremos destacar que aunque la entrada sean 4 bits,
Python internamente rellena todo lo que está a la izquierda con ceros.

a=0b0001

print(bin(a<<10))

# 0b10000000000

Operadores de Identidad

El operador de identidad o identity operator is nos indica si dos variables hacen referencia al mismo
objeto. Esto implica que si dos variables distintas tienen el mismo id(), el resultado de aplicar el
operador is sobre ellas será True.
Operador Nombre

is Devuelve True si hacen referencia a el mismo objeto

is not Devuelve False si no hacen referencia a el mismo objeto

Operador is

El operador is comprueba si dos variables hacen referencia a el mismo objeto. En el siguiente


ejemplo podemos ver como al aplicarse sobre a y b el resultado es True.

a = 10

b = 10

print(a is b) # True

Esto es debido a que Python reutiliza el mismo objeto que almacena el valor 10 para ambas
variables. De hecho, si usamos la función id(), podemos ver que el objeto es el mismo.

print(id(a)) # 4397849536

print(id(b)) # 4397849536

Podemos ver como también, ambos valores son iguales con el operador relacional ==, pero esto es
una mera casualidad como veremos a continuación. Que dos variables tengan el mismo contenido,
no implica necesariamente que hagan referencia a el mismo objeto.

print(a == b) # True

En el siguiente ejemplo, podemos ver como a y b almacenan el mismo valor, por lo que == nos
indica True.

a = [1, 2, 3]

b = [1, 2, 3]

print(a == b) # True

print(a is b) # False

Sin embargo, por como Python funciona por debajo, almacena el contenido en dos objetos
diferentes. Al tratarse de objetos diferentes, esto hace que el operador is devuelva False.

A diferencia de antes, podemos ver como la función id() en este caso nos devuelve un valor
diferente.

print(id(a)) # 4496626880

print(id(b)) # 4496626816
Esta diferencia puede resultar algo liosa, por lo que te recomendamos que leas más acerca de
la mutabilidad en Python.

Para saber más: Si quieres saber más acerca del operador id() te dejamos este enlace a la
documentación oficial.

Operador is not

Una vez definido is, es trivial definir is not porque es exactamente lo contrario.
Devuelve True cuando ambas variables no hacen referencia al mismo objeto.

# Python crea dos objetos diferentes, uno

# para cada lista. Las listas son mutables.

a = [1, 2, 3]

b = [1, 2, 3]

print(a is not b) # True

# Python reutiliza el objeto que almacena 5

# por lo que ambas variables apuntan a el mismo

a=5

b=5

print(a is not b) # False

Para saber más: Te dejamos un enlace muy interesante con más información sobre el is y is not.

Operadores de membresía

Los operadores de membresía o membership operators son operadores que nos permiten saber si
un elemento esta contenido en una secuencia. Por ejemplo si un número está contenido en una
lista de números.

Operador Nombre Ejemplo

in True si el elemento esta contenido xxx

not in False si el elemento no esta contenido xxx

Operador in

El operador in nos permite ver si un elemento esta contenido dentro de una secuencia, como podría
ser una lista. En el siguiente ejemplo se ve un caso sencillo donde se verifica si 3 esta contenido en
la lista [1, 2, 3]. Como efectivamente lo está, el resultado es True.
print(3 in [1, 2, 3])

# True

Vamos a complicar las cosas un poco y explorar los límites del operador. Que pasaría si
intentásemos hacer algo como lo que se ve en el siguiente ejemplo. Podría ser lógico pensar que 3
in 3 sería True, porque realmente si que parece que el 3 esta contenido en el segundo 3. Pues no, el
siguiente código daría un error, diciendo que la clase int no es iterable. En otros capítulos
exploraremos más acerca de esto. Por ahora nos basta con decir que el elemento a la derecha
del in debe ser un objeto tipo lista

#print(3 in 3) # Error! TypeError

Vamos a darle una última vuelta de tuerca. Podríamos también ver si una lista está contenida en
otra lista. En este caso, la lista de la derecha del in es una lista embebida dentro de otra lista.
Como [1, 2] está dentro de la segunda lista, el resultado es True

print([1, 2] in [4, [1, 2], 7])

# True

Operador not in

Por último, el operador not in realiza lo contrario al operador in. Verifica que un elemento no está
contenido en otra secuencia. En el siguiente ejemplo se puede ver como 3 no es parte de la
secuencia, por lo que el resultado es False

print(3 not in [1, 2, 4, 5])

# True

La verdad que ambos operadores in y not in son muy útiles y nos ahorran mucho trabajo. Es
importante tenerlo en cuenta, porque no otros lenguajes de programación no existen tales
operadores, y debemos escribir código extra para obtener tal funcionalidad. Una forma de
implementar nuestro operador in y is not con una función sería la siguiente. Simplemente iteramos
la lista y si encontramos el elemento que estábamos buscando devolvemos True, de lo
contrario False.

a=3

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

# Función que implementa "is" y "is not"

def estaContenido(a, lista):

for l in lista:

if a==l:
return True

return False

print(estaContenido(a, lista))

Para saber más: Te dejamos un enlace a la documentación oficial acerca de los operadores de
membresía.

Operador Walrus

Introducción

El operador walrus o walrus operator se introdujo con la PEP572 en Python 3.8, y se trata de un
operador de asignación con una funcionalidad extra que veremos a continuación. El operador se
representa con dos puntos seguidos de un igual :=, lo que tiene cierto parecido a una morsa,
siendo : los ojos y = los colmillos, por lo que de ahí viene su nombre (walrus significa morsa en
Inglés).

El problema que dicho operador intenta resolver es el siguiente. Podemos simplificar las siguientes
tres líneas de código.

x = "Python"

print(x)

print(type(x))

# Salida:

# Python

# <class 'str'>

En sólo dos líneas. Como podemos ver, el uso de := asigna y devuelve el contenido de la variable.

print(x := "Python")

print(type(x))

# Python

# <class 'str'>

Para gente que venga de otros lenguajes de programación como C, tal vez le resulte raro este
operador, ya que C permite realizar lo siguiente. Podemos ver como (x = 5) puede ser comparado
con == 5. Sin embargo esta sintaxis no es válida en Python, y el operador wallrus es precisamente lo
que intenta resolver.

#include <stdio.h>
int main(){

int x;

if ((x = 5) == 5) {

printf("Hola");

return 0;

Ejemplos Operador Walrus

En el siguiente programa pedimos al usuario que introduzca un texto y lo vamos añadiendo a una
lista hasta que el texto introducido sea “terminar”. Sin el operador walrus se podría escribir de la
siguiente forma.

lista = []

entrada = input("Escribe algo: ")

while entrada != "terminar":

lista.append(entrada)

entrada = input("Escribe algo: ")

print(lista)

Sin embargo aprovechando que el operador walrus := devuelve el contenido que es asignado,
podemos usar entrada para comparar con != "terminar". Es decir, podemos unir la asignación = y la
comparación en la misma línea. Esto nos permite ahorrar alguna línea de código.

lista = []

while (entrada := input("Escribe algo: ")) != "terminar":

lista.append(entrada)

print(lista)

También nos podemos ahorrar una línea de código si queremos primero asignar a una variable y
luego emplear dicha variable en, por ejemplo, un if.

a = 20*[1]

if (n := len(a)) > 10:


print(f"La lista tiene {n} elementos (>10)")

Por otro lado puede ser útil cuando usemos list comprehensions donde el valor que usamos para
filtrar es modificado y se necesita también en el cuerpo del bucle.

resultado = [(x, y, x/y) for x in datos if (y := f(x)) > 0]

De manera similar, podemos reutilizar el resultado de una expresión evitando tener que volver a
computarla.

lista = [[y := f(x), x/y] for x in range(5)]

Puedes encontrar otros ejemplos en los siguientes enlaces:

• En la documentación oficial de Python

• En la propia sección de ejemplos de la PEP572.

• Y en este pull request.

Críticas Al Operador Walrus

El operador walrus ha recibido muchas críticas entre los desarrolladores de Python,


principalmente porque aunque sea cierto que su uso puede ahorrarnos alguna línea de código, a
veces empeora la legibilidad. No siempre menos código implica una mejora, sobre todo si se hace
más complicado de leer.

Por otro lado, aunque el operador walrus sea muy similar al operador de asignación =, su uso no es
consistente. Por ejemplo, el siguiente código no es correcto usando :=.

# Incorrecto

class Example:

[(j := i) for i in range(5)]

# SyntaxError: assignment expression within a comprehension cannot be used in a class body

De hecho, el propio creador Christoph Groth sugirió en el siguiente hilo que tal vez sería
conveniente implementar el uso de = como en C/C++ (recuerda el ejemplo que hemos visto
anteriormente). Si esto se llega a producir, no sabemos que podría pasar con el operador walrus,
pero tal vez ya no sería necesario.

Programación Orientada a Objetos

• 📗📗 Programación Orientada a Objetos

• 📙📙 Tipos de métodos

• 📙📙 Herencia

• 📙📙 Decorador Property
• 📙📙 Métodos dunder o mágicos

• 📕📕 Sobreescribiendo métodos mágicos

• 📕📕 Interfaces y Abstract Base Class

• 📗📗 Abstracción

• 📗📗 Acoplamiento

• 📗📗 Crear clase

• 📗📗 Encapsulamiento

• 📙📙 Polimorfismo

• 📗📗 Cohesión

Programación Orientada a Objetos

Antes de nada, empecemos con una introducción básica a la Programación Orientada


a Objetos POO o OOP en inglés. Se trata de un paradigma de programación introducido en los años
1970s, pero que no se hizo popular hasta años más tarde.

Este modo o paradigma de programación nos permite organizar el código de una manera que se
asemeja bastante a como pensamos en la vida real, utilizando las famosas clases. Estas nos
permiten agrupar un conjunto de variables y funciones que veremos a continuación.

Cosas de lo más cotidianas como un perro o un coche pueden ser representadas con clases. Estas
clases tienen diferentes características, que en el caso del perro podrían ser la edad, el nombre o
la raza. Llamaremos a estas características, atributos.

Por otro lado, las clases tienen un conjunto de funcionalidades o cosas que pueden hacer. En el
caso del perro podría ser andar o ladrar. Llamaremos a estas funcionalidades métodos.

Por último, pueden existir diferentes tipos de perro. Podemos tener uno que se llama Toby o el del
vecino que se llama Laika. Llamaremos a estos diferentes tipos de perro objetos. Es decir, el
concepto abstracto de perro es la clase, pero Toby o cualquier otro perro particular será el objeto.

La programación orientada a objetos está basada en 6 principios o pilares básicos:

• Herencia

• Cohesión

• Abstracción

• Polimorfismo

• Acoplamiento

• Encapsulamiento
Motivación

Una vez explicada la programación orientada a objetos puede parecer bastante lógica, pero no es
algo que haya existido siempre, y de hecho hay muchos lenguajes de programación que no la
soportan.

En parte surgió debido a la creciente complejidad a la que los programadores se iban enfrentando
conforme pasaban los años. En el mundo de la programación hay gran cantidad de aplicaciones
que realizan tareas muy similares y es importante identificar los patrones que nos permiten no
reinventar la rueda. La programación orientada a objetos intentaba resolver esto.

Uno de los primeros mecanismos que se crearon fueron las funciones, que permiten agrupar
bloques de código que hacen una tarea específica bajo un nombre. Algo muy útil ya que permite
también reusar esos módulos o funciones sin tener que copiar todo el código, tan solo la llamada.

Las funciones resultaron muy útiles, pero no eran capaces de satisfacer todas las necesidades de
los programadores. Uno de los problemas de las funciones es que sólo realizan unas operaciones
con unos datos de entrada para entregar una salida, pero no les importa demasiado conservar esos
datos o mantener algún tipo de estado. Salvo que se devuelva un valor en la llamada a la función o
se usen variables globales, todo lo que pasa dentro de la función queda olvidado, y esto en muchos
casos es un problema.

Imaginemos que tenemos un juego con naves espaciales moviéndose por la pantalla. Cada nave
tendrá una posición (x,y) y otros parámetros como el tipo de nave, su color o tamaño. Sin hacer uso
de clases y POO, tendremos que tener una variable para cada dato que queremos almacenar:
coordenadas, color, tamaño, tipo. El problema viene si tenemos 10 naves, ya que nos podríamos
juntar con un número muy elevado de variables. Todo un desastre.

En el mundo de la programación existen tareas muy similares al ejemplo con las naves, y en
respuesta a ello surgió la programación orientada a objetos. Una herramienta perfecta que permite
resolver cierto tipo de problemas de una forma mucho más sencilla, con menos código y más
organizado. Agrupa bajo una clase un conjunto de variables y funciones, que pueden ser
reutilizadas con características particulares creando objetos.

Definiendo clases

Vista ya la parte teórica, vamos a ver como podemos hacer uso de la programación orientada a
objetos en Python. Lo primero es crear una clase, para ello usaremos el ejemplo de perro.

# Creando una clase vacía

class Perro:

pass

Se trata de una clase vacía y sin mucha utilidad práctica, pero es la mínima clase que podemos
crear. Nótese el uso del pass que no hace realmente nada, pero daría un error si después de los : no
tenemos contenido.
Ahora que tenemos la clase, podemos crear un objeto de la misma. Podemos hacerlo como si de
una variable normal se tratase. Nombre de la variable igual a la clase con (). Dentro de los
paréntesis irían los parámetros de entrada si los hubiera.

# Creamos un objeto de la clase perro

mi_perro = Perro()

Definiendo atributos

A continuación vamos a añadir algunos atributos a nuestra clase. Antes de nada es importante
distinguir que existen dos tipos de atributos:

• Atributos de instancia: Pertenecen a la instancia de la clase o al objeto. Son atributos


particulares de cada instancia, en nuestro caso de cada perro.

• Atributos de clase: Se trata de atributos que pertenecen a la clase, por lo tanto serán
comunes para todos los objetos.

Empecemos creando un par de atributos de instancia para nuestro perro, el nombre y la raza. Para
ello creamos un método __init__ que será llamado automáticamente cuando creemos un objeto. Se
trata del constructor.

class Perro:

# El método __init__ es llamado al crear el objeto

def __init__(self, nombre, raza):

print(f"Creando perro {nombre}, {raza}")

# Atributos de instancia

self.nombre = nombre

self.raza = raza

Ahora que hemos definido el método init con dos parámetros de entrada, podemos crear el objeto
pasando el valor de los atributos. Usando type() podemos ver como efectivamente el objeto es de la
clase Perro.

mi_perro = Perro("Toby", "Bulldog")

print(type(mi_perro))

# Creando perro Toby, Bulldog

# <class '__main__.Perro'>

Seguramente te hayas fijado en el self que se pasa como parámetro de entrada del método. Es una
variable que representa la instancia de la clase, y deberá estar siempre ahí.
El uso de __init__ y el doble __ no es una coincidencia. Cuando veas un método con esa forma,
significa que está reservado para un uso especial del lenguaje. En este caso sería lo que se conoce
como constructor. Hay gente que llama a estos métodos mágicos.

Por último, podemos acceder a los atributos usando el objeto y .. Por lo tanto.

print(mi_perro.nombre) # Toby

print(mi_perro.raza) # Bulldog

Hasta ahora hemos definido atributos de instancia, ya que son atributos que pertenecen a cada
perro concreto. Ahora vamos a definir un atributo de clase, que será común para todos los perros.
Por ejemplo, la especie de los perros es algo común para todos los objetos Perro.

class Perro:

# Atributo de clase

especie = 'mamífero'

# El método __init__ es llamado al crear el objeto

def __init__(self, nombre, raza):

print(f"Creando perro {nombre}, {raza}")

# Atributos de instancia

self.nombre = nombre

self.raza = raza

Dado que es un atributo de clase, no es necesario crear un objeto para acceder al atributos.
Podemos hacer lo siguiente.

print(Perro.especie)

# mamífero

Se puede acceder también al atributo de clase desde el objeto.

mi_perro = Perro("Toby", "Bulldog")

mi_perro.especie

# 'mamífero'

De esta manera, todos los objetos que se creen de la clase perro compartirán ese atributo de clase,
ya que pertenecen a la misma.

Definiendo métodos
En realidad cuando usamos __init__ anteriormente ya estábamos definiendo un método, solo que
uno especial. A continuación vamos a ver como definir métodos que le den alguna funcionalidad
interesante a nuestra clase, siguiendo con el ejemplo de perro.

Vamos a codificar dos métodos, ladrar y caminar. El primero no recibirá ningún parámetro y el
segundo recibirá el número de pasos que queremos andar. Como hemos indicado
anteriormente self hace referencia a la instancia de la clase. Se puede definir un método con def y
el nombre, y entre () los parámetros de entrada que recibe, donde siempre tendrá que estar self el
primero.

class Perro:

# Atributo de clase

especie = 'mamífero'

# El método __init__ es llamado al crear el objeto

def __init__(self, nombre, raza):

print(f"Creando perro {nombre}, {raza}")

# Atributos de instancia

self.nombre = nombre

self.raza = raza

def ladra(self):

print("Guau")

def camina(self, pasos):

print(f"Caminando {pasos} pasos")

Por lo tanto si creamos un objeto mi_perro, podremos hacer uso de sus métodos llamándolos
con . y el nombre del método. Como si de una función se tratase, pueden recibir y devolver
argumentos.

mi_perro = Perro("Toby", "Bulldog")

mi_perro.ladra()

mi_perro.camina(10)
# Creando perro Toby, Bulldog

# Guau

# Caminando 10 pasos

Si te ha parecido útil este post, tal vez te interese:

• Uso del decorador property

• Métodos estáticos y de clase

Métodos en Python: instancia, clase y estáticos

En otros posts hemos visto como se pueden crear métodos con def dentro de una clase, pudiendo
recibir parámetros como entrada y modificar el estado (como los atributos) de la instancia. Pues
bien, haciendo uso de los decoradores, es posible crear diferentes tipos de métodos:

• Lo métodos de instancia “normales” que ya hemos visto como metodo()

• Métodos de clase usando el decorador @classmethod

• Y métodos estáticos usando el decorador @staticmethod

En la siguiente clase tenemos un ejemplo donde definimos los tres tipos de métodos.

class Clase:

def metodo(self):

return 'Método normal', self

@classmethod

def metododeclase(cls):

return 'Método de clase', cls

@staticmethod

def metodoestatico():

return "Método estático"

Veamos su comportamiento en detalle uno por uno.

Métodos de instancia
Los métodos de instancia son los métodos normales, de toda la vida, que hemos visto
anteriormente. Reciben como parámetro de entrada self que hace referencia a la instancia que
llama al método. También pueden recibir otros argumentos como entrada.

Para saber más: El uso de "self" es totalmente arbitrario. Se trata de una convención acordada por
los usuarios de Python, usada para referirse a la instancia que llama al método, pero podría ser
cualquier otro nombre. Lo mismo ocurre con "cls", que veremos a continuación.

class Clase:

def metodo(self, arg1, arg2):

return 'Método normal', self

Y como ya sabemos, una vez creado un objeto pueden ser llamados.

mi_clase = Clase()

mi_clase.metodo("a", "b")

# ('Método normal', <__main__.Clase at 0x10b9daa90>)

En vista a esto, los métodos de instancia:

• Pueden acceder y modificar los atributos del objeto.

• Pueden acceder a otros métodos.

• Dado que desde el objeto self se puede acceder a la clase con ` self.class`, también pueden
modificar el estado de la clase

Métodos de clase (classmethod)

A diferencia de los métodos de instancia, los métodos de clase reciben como argumento cls, que
hace referencia a la clase. Por lo tanto, pueden acceder a la clase pero no a la instancia.

class Clase:

@classmethod

def metododeclase(cls):

return 'Método de clase', cls

Se pueden llamar sobre la clase.

Clase.metododeclase()

# ('Método de clase', __main__.Clase)

Pero también se pueden llamar sobre el objeto.

mi_clase.metododeclase()

# ('Método de clase', __main__.Clase)


Por lo tanto, los métodos de clase:

• No pueden acceder a los atributos de la instancia.

• Pero si pueden modificar los atributos de la clase.

Métodos estáticos (staticmethod)

Por último, los métodos estáticos se pueden definir con el decorador @staticmethod y no aceptan
como parámetro ni la instancia ni la clase. Es por ello por lo que no pueden modificar el estado ni
de la clase ni de la instancia. Pero por supuesto pueden aceptar parámetros de entrada.

class Clase:

@staticmethod

def metodoestatico():

return "Método estático"

mi_clase = Clase()

Clase.metodoestatico()

mi_clase.metodoestatico()

# 'Método estático'

# 'Método estático'

Por lo tanto el uso de los métodos estáticos pueden resultar útil para indicar que un método no
modificará el estado de la instancia ni de la clase. Es cierto que se podría hacer lo mismo con un
método de instancia por ejemplo, pero a veces resulta importante indicar de alguna manera estas
peculiaridades, evitando así futuros problemas y malentendidos.

En otras palabras, los métodos estáticos se podrían ver como funciones normales, con la salvedad
de que van ligadas a una clase concreta.

Herencia en Python

Para entender la herencia, es fundamental entender la programación orientada a objetos, por lo


que te recomendamos empezar por ahí antes.

La herencia es un proceso mediante el cual se puede crear una clase hija que hereda de una
clase padre, compartiendo sus métodos y atributos. Además de ello, una clase hija puede
sobreescribir los métodos o atributos, o incluso definir unos nuevos.

Se puede crear una clase hija con tan solo pasar como parámetro la clase de la que queremos
heredar. En el siguiente ejemplo vemos como se puede usar la herencia en Python, con la
clase Perro que hereda de Animal. Así de fácil.
# Definimos una clase padre

class Animal:

pass

# Creamos una clase hija que hereda de la padre

class Perro(Animal):

pass

De hecho podemos ver como efectivamente la clase Perro es la hija de Animal usando __bases__

print(Perro.__bases__)

# (<class '__main__.Animal'>,)

De manera similar podemos ver que clases descienden de una en concreto con __subclasses__.

print(Animal.__subclasses__())

# [<class '__main__.Perro'>]

¿Y para que queremos la herencia? Dado que una clase hija hereda los atributos y métodos de la
padre, nos puede ser muy útil cuando tengamos clases que se parecen entre sí pero tienen ciertas
particularidades. En este caso en vez de definir un montón de clases para cada animal, podemos
tomar los elementos comunes y crear una clase Animal de la que hereden el resto, respetando por
tanto la filosofía DRY. Realizar estas abstracciones y buscar el denominador común para definir una
clase de la que hereden las demás, es una tarea de lo más compleja en el mundo de la
programación.

Para saber más: El principio DRY (Don't Repeat Yourself) es muy aplicado en el mundo de la
programación y consiste en no repetir código de manera innecesaria. Cuanto más código duplicado
exista, más difícil será de modificar y más fácil será crear inconsistencias. Las clases y la herencia
a no repetir código.

Extendiendo y modificando métodos

Continuemos con nuestro ejemplo de perros y animales. Vamos a definir una clase
padre Animal que tendrá todos los atributos y métodos genéricos que los animales pueden tener.
Esta tarea de buscar el denominador común es muy importante en programación. Veamos los
atributos:

• Tenemos la especie ya que todos los animales pertenecen a una.

• Y la edad, ya que todo ser vivo nace, crece, se reproduce y muere.

Y los métodos o funcionalidades:


• Tendremos el método hablar, que cada animal implementará de una forma. Los perros
ladran, las abejas zumban y los caballos relinchan.

• Un método moverse. Unos animales lo harán caminando, otros volando.

• Y por último un método descríbeme que será común.

Definimos la clase padre, con una serie de atributos comunes para todos los animales como
hemos indicado.

class Animal:

def __init__(self, especie, edad):

self.especie = especie

self.edad = edad

# Método genérico pero con implementación particular

def hablar(self):

# Método vacío

pass

# Método genérico pero con implementación particular

def moverse(self):

# Método vacío

pass

# Método genérico con la misma implementación

def describeme(self):

print("Soy un Animal del tipo", type(self).__name__)

Tenemos ya por lo tanto una clase genérica Animal, que generaliza las características y
funcionalidades que todo animal puede tener. Ahora creamos una clase Perro que hereda
del Animal. Como primer ejemplo vamos a crear una clase vacía, para ver como los métodos y
atributos son heredados por defecto.

# Perro hereda de Animal

class Perro(Animal):
pass

mi_perro = Perro('mamífero', 10)

mi_perro.describeme()

# Soy un Animal del tipo Perro

Con tan solo un par de líneas de código, hemos creado una clase nueva que tiene todo el contenido
que la clase padre tiene, pero aquí viene lo que es de verdad interesante. Vamos a crear varios
animales concretos y sobreescrbir algunos de los métodos que habían sido definidos en la
clase Animal, como el hablar o el moverse, ya que cada animal se comporta de una manera
distinta.

Podemos incluso crear nuevos métodos que se añadirán a los ya heredados, como en el caso de
la Abeja con picar().

class Perro(Animal):

def hablar(self):

print("Guau!")

def moverse(self):

print("Caminando con 4 patas")

class Vaca(Animal):

def hablar(self):

print("Muuu!")

def moverse(self):

print("Caminando con 4 patas")

class Abeja(Animal):

def hablar(self):

print("Bzzzz!")

def moverse(self):

print("Volando")
# Nuevo método

def picar(self):

print("Picar!")

Por lo tanto ya podemos crear nuestros objetos de esos animales y hacer uso de sus métodos que
podrían clasificarse en tres:

• Heredados directamente de la clase padre: describeme()

• Heredados de la clase padre pero modificados: hablar() y moverse()

• Creados en la clase hija por lo tanto no existentes en la clase padre: picar()

mi_perro = Perro('mamífero', 10)

mi_vaca = Vaca('mamífero', 23)

mi_abeja = Abeja('insecto', 1)

mi_perro.hablar()

mi_vaca.hablar()

# Guau!

# Muuu!

mi_vaca.describeme()

mi_abeja.describeme()

# Soy un Animal del tipo Vaca

# Soy un Animal del tipo Abeja

mi_abeja.picar()

# Picar!

Uso de super()

En pocas palabras, la función super() nos permite acceder a los métodos de la clase padre desde
una de sus hijas. Volvamos al ejemplo de Animal y Perro.

class Animal:
def __init__(self, especie, edad):

self.especie = especie

self.edad = edad

def hablar(self):

pass

def moverse(self):

pass

def describeme(self):

print("Soy un Animal del tipo", type(self).__name__)

Tal vez queramos que nuestro Perro tenga un parámetro extra en el constructor, como podría ser
el dueño. Para realizar esto tenemos dos alternativas:

• Podemos crear un nuevo __init__ y guardar todas las variables una a una.

• O podemos usar super() para llamar al __init__ de la clase padre que ya aceptaba
la especie y edad, y sólo asignar la variable nueva manualmente.

class Perro(Animal):

def __init__(self, especie, edad, dueño):

# Alternativa 1

# self.especie = especie

# self.edad = edad

# self.dueño = dueño

# Alternativa 2

super().__init__(especie, edad)

self.dueño = dueño

mi_perro = Perro('mamífero', 7, 'Luis')

mi_perro.especie
mi_perro.edad

mi_perro.dueño

Herencia múltiple

En Python es posible realizar herencia múltiple. En otros posts hemos visto como se podía crear
una clase padre que heredaba de una clase hija, pudiendo hacer uso de sus métodos y atributos. La
herencia múltiple es similar, pero una clase hereda de varias clases padre en vez de una sola.

Veamos un ejemplo. Por un lado tenemos dos clases Clase1 y Clase2, y por otro tenemos
la Clase3 que hereda de las dos anteriores. Por lo tanto, heredará todos los métodos y atributos de
ambas.

class Clase1:

pass

class Clase2:

pass

class Clase3(Clase1, Clase2):

pass

Es posible también que una clase herede de otra clase y a su vez otra clase herede de la anterior.

class Clase1:

pass

class Clase2(Clase1):

pass

class Clase3(Clase2):

pass

Llegados a este punto nos podemos plantear lo siguiente. Vale, como sabemos de otros posts las
clases hijas heredan los métodos de las clases padre, pero también pueden reimplementarlos de
manera distinta. Entonces, si llamo a un método que todas las clases tienen en común ¿a cuál se
llama?. Pues bien, existe una forma de saberlo.

La forma de saber a que método se llama es consultar el MRO o Method Order Resolution. Esta
función nos devuelve una tupla con el orden de búsqueda de los métodos. Como era de esperar se
empieza en la propia clase y se va subiendo hasta la clase padre, de izquierda a derecha.

class Clase1:

pass

class Clase2:
pass

class Clase3(Clase1, Clase2):

pass

print(Clase3.__mro__)

# (<class '__main__.Clase3'>, <class '__main__.Clase1'>, <class '__main__.Clase2'>, <class 'object'>)

Una curiosidad es que al final del todo vemos la clase object. Aunque pueda parecer raro, es
correcto ya que en realidad todas las clases en Python heredan de una clase genérica object,
aunque no lo especifiquemos explícitamente.

Y como último ejemplo,…el cielo es el límite. Podemos tener una clase heredando de otras tres.
Fíjate en que el MRO depende del orden en el que las clases son pasadas: 1, 3, 2.

class Clase1:

pass

class Clase2:

pass

class Clase3:

pass

class Clase4(Clase1, Clase3, Clase2):

pass

print(Clase4.__mro__)

# (<class '__main__.Clase4'>, <class '__main__.Clase1'>, <class '__main__.Clase3'>, <class


'__main__.Clase2'>, <class 'object'>)

Junto con la herencia,


la cohesión, abstracción, polimorfismo, acoplamiento y encapsulamiento son otros de los
conceptos claves para entender la programación orientada a objetos.

Decorador Property

En otros tutoriales hemos visto como se crean y usan los decoradores en Python. A continuación
veremos el decorador @property, que viene por defecto con Python, y puede ser usado para
modificar un método para que sea un atributo o propiedad. Es importante que conozcan antes la
programación orientada a objetos.

El decorador puede ser usado sobre un método, que hará que actúe como si fuera un atributo.

class Clase:
def __init__(self, mi_atributo):

self.__mi_atributo = mi_atributo

@property

def mi_atributo(self):

return self.__mi_atributo

Como si de un atributo normal se tratase, podemos acceder a el con el objeto . y nombre.

mi_clase = Clase("valor_atributo")

mi_clase.mi_atributo

# 'valor_atributo'

Muy importante notar que aunque mi_atributo pueda parecer un método, en realidad no lo es, por
lo que no puede ser llamado con ().

# mi_clase.mi_atributo() # Error! Es un atributo, no un método

Tal vez te preguntes para que sirve esto, ya que el siguiente código hace exactamente lo mismo sin
hacer uso de decoradores.

class Clase:

def __init__(self, mi_atributo):

self.mi_atributo = mi_atributo

mi_clase = Clase("valor_atributo")

mi_clase.mi_atributo

# 'valor_atributo'

Bien, la explicación no es sencilla, pero está relacionada con el concepto de encapsulación de la


programación orientada a objetos. Este concepto nos indica que en determinadas ocasiones es
importante ocultar el estado interno de los objetos al exterior, para evitar que sean modificados de
manera incorrecta. Para la gente que venga del mundo de Java, esto no será nada nuevo, y está muy
relacionado con los métodos set()y get() que veremos a continuación.

La primera diferencia que vemos entre los códigos anteriores es el uso de __ antes de mi_atributo.
Cuando nombramos una variable de esta manera, es una forma de decirle a Python que queremos
que se “oculte” y que no pueda ser accedida como el resto de atributos.

class Clase:
def __init__(self, mi_atributo):

self.__mi_atributo = mi_atributo

mi_clase = Clase("valor_atributo")

# mi_clase.__mi_atributo # Error!

Esto puede ser importante con ciertas variables que no queremos que sean accesibles desde el
exterior de una manera no controlada. Al definir la propiedad con @property el acceso a ese
atributo se realiza a través de una función, siendo por lo tanto un acceso controlado.

class Clase:

def __init__(self, mi_atributo):

self.__mi_atributo = mi_atributo

@property

def mi_atributo(self):

# El acceso se realiza a través de este "método" y

# podría contener código extra y no un simple retorno

return self.__mi_atributo

Otra utilidad podría ser la consulta de un parámetro que requiera de muchos cálculos. Se podría
tener un atributo que no estuviera directamente almacenado en la clase, sino que precisara de
realizar ciertos cálculos. Para optimizar esto, se podrían hacer los cálculos sólo cuando el atributo
es consultado.

Por último, existen varios añadidos al decorador @property como pueden ser el setter. Se trata de
otro decorador que permite definir un “método” que modifica el contenido del atributo que se esté
usando.

class Clase:

def __init__(self, mi_atributo):

self.__mi_atributo = mi_atributo

@property

def mi_atributo(self):
return self.__mi_atributo

@mi_atributo.setter

def mi_atributo(self, valor):

if valor != "":

print("Modificando el valor")

self.__mi_atributo = valor

else:

print("Error está vacío")

De esta forma podemos añadir código al setter, haciendo que por ejemplo realice comprobaciones
antes de modificar el valor. Esto es una cosa que de usar un atributo normal no podríamos hacer, y
es muy útil de cara a la encapsulación.

mi_clase = Clase("valor_atributo")

mi_clase.mi_atributo

# 'valor_atributo'

mi_clase.mi_atributo = "nuevo_valor"

mi_clase.mi_atributo

# 'nuevo_valor'

mi_clase.mi_atributo = ""

# Error está vacío

Resulta lógico pensar que si un determinado atributo pertenece a una clase, si queremos
modificarlo debería de tener la “aprobación” de la clase, para asegurarse que ninguna entidad
externa está “haciendo cosas raras”.

Métodos dunder o mágicos

Una de las características menos conocidas pero valiosas de Python es la capacidad de


implementar métodos mágicos en los objetos. Usando métodos mágicos, podemos escribir código
más limpio que sea intuitivo y fácil de entender.
Con métodos mágicos, podemos crear interfaces para interactuar con objetos de una manera que
se siente más Python. Este artículo le introducirá a los métodos mágicos, discutirá las mejores
prácticas para crearlos y explorará los métodos mágicos comunes que encontrará.

¿Qué son los métodos mágicos?

Los métodos mágicos son métodos Python que definen cómo se comportan los objetos Python
cuando se realizan operaciones comunes sobre ellos. Estos métodos se definen claramente con
guiones bajos dobles antes y después del nombre del método.

Como resultado, son comúnmente llamados métodos dunder, como en double underscore. Un
método dunder común que puede que ya haya encontrado es el método __init__() que se utiliza para
definir constructores de clases.

Normalmente, los métodos dunder no están pensados para ser llamados directamente en su
código; más bien, serán llamados por el intérprete mientras se ejecuta el programa.

¿Por qué son útiles los métodos mágicos?

Los métodos mágicos son un concepto útil en la Programación Orientada a Objetos en Python.
Utilizándolos, usted especifica el comportamiento de sus tipos de datos personalizados cuando se
utilizan con operaciones comunes incorporadas. Estas operaciones incluyen:

� Operaciones aritméticas

� Operaciones de comparación

� Operaciones de ciclo de vida

� Operaciones de representación

La siguiente sección tratará sobre cómo implementar métodos mágicos que definan cómo se
comporta la aplicación cuando se utiliza en todas las categorías anteriores.

Cómo definir métodos mágicos

Como se ha mencionado anteriormente, los métodos mágicos especifican el comportamiento de


los objetos. Como tales, se definen como parte de la clase del objeto. Dado que forman parte de la
clase del objeto, toman como primer argumento self, que es una referencia al propio objeto.

Pueden tomar argumentos adicionales dependiendo de cómo vayan a ser llamados por el
intérprete. También se definen claramente con dos guiones bajos antes y después de sus nombres.

Implementación

Gran parte de lo que hemos discutido hasta ahora parece teórico y abstracto. En esta sección,
implementaremos una simple clase Rectángulo.

Esta clase tendrá propiedades de longitud y anchura. Utilizando el método __init__, podrá
especificar estas propiedades en la instanciación. Además, podrá comparar diferentes rectángulos
para ver si es igual, menor o mayor que otro utilizando los operadores ==, < y >. Por último, el
rectángulo debe ser capaz de proporcionar una representación de cadena significativa.

Configuración del entorno de codificación

Para seguir este tutorial, necesitará un entorno de ejecución Python. Puede utilizar uno local, o
puede utilizar el compilador de Python en línea de Geekflare.

Creación de la clase rectángulo

En primer lugar, empecemos por definir la clase Rectángulo.

clase Rectángulo:

pass

Copy

Creación del método constructor

A continuación, vamos a crear nuestro primer método mágico, el método constructor de la clase.
Este método tomará la altura y la anchura y las almacenará como atributos en la instancia de la
clase.

clase Rectángulo:

def __init__(self, altura, anchura):

self.altura = altura

self.anchura = anchura

Copy

Creación de un método mágico para la representación de cadenas

A continuación, queremos crear un método que permita a nuestra clase generar una cadena legible
por humanos para representar el objeto. Este método será llamado siempre que llamemos a la
función str( ) pasando una instancia de la clase Rectángulo como argumento. Este método también
será llamado cuando llamemos a funciones que esperan un argumento de cadena, como la
función print.

clase Rectángulo:

def __init__(self, altura, anchura):

self.altura = altura

self.anchura = anchura

def __str__(self):
return f'Rectángulo({altura.self}, {anchura.self})'

Copy

El método __str__( ) debe devolver una cadena que usted quiera que represente al objeto. En este
caso, estamos devolviendo una cadena del formato Rectángulo(<altura>, <anchura>) donde altura y
anchura son las dimensiones almacenadas del rectángulo.

Creación de métodos mágicos para operaciones de comparación

A continuación, queremos crear operadores de comparación para las operaciones igual a, menor
que y mayor que. Esto se denomina sobrecarga de operadores. Para crearlos, utilizaremos los
métodos mágicos __eq__, __lt__ y __gt__ respectivamente. Estos métodos devolverán un valor
booleano después de comparar las áreas de los rectángulos.

clase Rectángulo:

def __init__(self, altura, anchura):

self.altura = altura

self.anchura = anchura

def __str__(self):

return f'Rectángulo({altura.self}, {anchura.self})'

def __eq__(self, otro):

""" Comprobación de igualdad """

return altura.auto * anchura.auto == altura.otro * anchura.otro

def __lt__(self, otro):

""" Comprobación de si el rectángulo es menor que el otro """

return altura.auto * anchura.auto < altura.otro * anchura.otro

def __gt__(self, otro):

""" Comprobar si el rectángulo es mayor que el otro """

return altura.propia * anchura.propia > altura.ajena * anchura.ajena

Copy
Como puede ver, estos métodos reciben dos parámetros. El primero es el rectángulo actual, y el
segundo es el otro valor con el que se está comparando. Este valor puede ser otra instancia de
Rectángulo o cualquier otro valor. La lógica de la comparación y las condiciones bajo las cuales la
comparación devolverá verdadero dependen completamente de usted.

Métodos mágicos comunes

En la siguiente sección, discutiremos los métodos mágicos comunes que encontrará y utilizará.

#1. Operaciones aritméticas


Los métodos mágicos aritméticos son llamados cuando una instancia de su clase se sitúa a la
izquierda de un signo aritmético. El método será llamado con dos argumentos, siendo el primero
una referencia a la instancia. El segundo es el objeto situado a la derecha del signo. Los métodos y
los signos son los siguientes:

Nombre Método Signo Descripción


Suma __add__ Implementa la suma.

Resta __sub__ – Implementa la sustracción.

Multiplicación __mul__ * Implementa la multiplicación

División __div__ / Implementa la división.

División de suelo __floordiv__ // Implementa la división del suelo.

#2. Operaciones de comparación


Al igual que los métodos aritméticos mágicos, estos métodos son llamados cuando una instancia
de la clase para la que están definidos se sitúa a la izquierda del operador de comparación.
También, como los métodos aritméticos mágicos, son llamados con dos parámetros; el primero es
una referencia a la instancia del objeto. El segundo es una referencia al valor situado a la derecha
del signo.
Nombre Método Signo Descripción

Menor que __lt__ < Implementa la comparación menor que

Mayor que __gt__ > Implementa la comparación mayor que

Igual a __eq__ == Implementa la comparación igual a

Menor o igual que __le__ >= Implementa la comparación menor o igual que

Mayor o igual que __ge__ <= Implementa la comparación mayor o igual que

#3. Operaciones del ciclo de vida


Estos métodos serán llamados en respuesta a los diferentes métodos del ciclo de vida de un
objeto, como ser instanciado o borrado. El constructor, __init__ entra dentro de esta categoría. Los
métodos comunes de esta categoría se enumeran en la siguiente tabla:

Nombre Método Descripción


Este método es llamado cada vez que se elimina un objeto de la clase para la que es
Constructor__init__ utilizarse para realizar acciones de limpieza, como cerrar cualquier archivo que hubi

Este método es llamado cada vez que se borra un objeto de la clase para la que está
Borrado __del__ para realizar acciones de limpieza como cerrar cualquier archivo que haya abierto.

El método __new__ es llamado en primer lugar cuando se instancia un objeto de la cl


método se llama antes que el constructor y toma la clase así como cualquier argume
Nuevo __nuevo__una instancia de la clase. En su mayor parte, no es demasiado útil, pero se cubre en

#4. Operaciones de representación

NombreMétodo Descripción

Devuelve una representación en forma de cadena legible por humanos del objeto. Se llama
se llama a la función str( ), pasando una instancia de la clase como argumento. También se
instancia a las funciones print() y format(). Su finalidad es proporcionar una cadena compr
Str __str__ final de la aplicación.
Devuelve una representación en forma de cadena del objeto que utiliza el desarrollador. Lo
devuelta sea rica en información, de forma que se pueda construir una instancia idéntica d
Repr __repr__la cadena.

Mejores prácticas para crear métodos mágicos

Los métodos mágicos son increíbles y simplificarán su código. Sin embargo, es importante que
tenga en cuenta lo siguiente cuando los utilice.

• Utilícelos con moderación – Implementar demasiados métodos mágicos en sus clases hace
que su código sea difícil de entender. Limítese a implementar sólo los esenciales.

• Asegúrese de que comprende las implicaciones para el rendimiento de métodos como


__setatrr__ y __getattr__ antes de utilizarlos.

• Documente el comportamiento de sus métodos mágicos para que otros desarrolladores


sepan exactamente lo que hacen. Así les resultará más fácil utilizarlos y depurarlos cuando
sea necesario.

Palabras finales

En este artículo, he introducido los métodos mágicos como una forma de crear clases que se
pueden utilizar con operaciones incorporadas. También hablé de cómo se definen y repasé un
ejemplo de una clase que implementa métodos mágicos. A continuación, mencioné los diferentes
métodos que probablemente va a utilizar y necesitar antes de compartir algunas de las mejores
prácticas a tener en cuenta.

A continuación, es posible que desee aprender a implementar la clase Contador en Python.

Sobreescribiendo métodos mágicos

Clases: métodos mágicos y propiedades

junio 13, 2018 by Recursos Python Dejar un comentario

Todo programador de Python que trabaje creando (y, en menor medida, también utilizando) clases
debe estar al tanto de los «métodos mágicos». Son aquellos que comienzan y terminan con doble
guión bajo; ya estarás al tanto de algunos, como __init__(), que no están pensados para ser
invocados manualmente sino que son llamados por Python en situaciones particulares (por
ejemplo, cuando se realiza una comparación entre dos instancias vía los operadores >, <, ==, etc. o
bien cuando se ejecuta alguna operación aritmética). Estaremos trabajando con varios de ellos y de
paso daremos un vistazo a las propiedades, que sirven para encapsular atributos de una clase.

Un conocimiento más o menos básico sobre las clases en general y cómo funcionan en Python en
particular sería una buena base para seguir este artículo. Si no estás muy familiarizado con estos
conceptos, te invito a chequear el artículo sobre Clases y orientación a objetos y la sección
de Clases en nuestro tutorial del lenguaje.

Primeros pasos
Iremos aprendiendo por ejemplos. Así que trabajaremos con una clase que llamaré Time, que
contendrá tres atributos: horas, minutos y segundos. Nuestro objetivo no es representar una hora
del día en particular, sino una unidad de tiempo en general, de modo que permitiremos valores
mayores a 24 horas y menores a 0.

1. class Time:

2.

3. def __init__(self, h=0, m=0, s=0):

4. self.h = h

5. self.m = m

6. self.s = s

Hasta aquí tenemos una primera definición de nuestra clase, que toma los argumentos que
mencionamos recién y los guarda en la instancia para que puedan ser accedidos como atributos.
Probémosla: intentemos representar las 14 horas con 23 minutos y 10 segundos.

1. a = Time(14, 23, 10)

2. print(a.h, a.m, a.s) # 14 23 10

Los argumentos tienen el valor cero por defecto; así, también es válido:

1. # Media hora.

2. b = Time(m=30)

3. print(b.h, b.m, b.s) # 0 30 0

Ahora bien, sería interesante poder imprimir directamente cualquier instancia de Time y que nos
muestre sus tres atributos. Si hacemos eso ahora, veremos que el resultado se ve más o menos así.

1. print(a) # <__main__.Time object at 0x027C6430>

Lo cual no nos dice mucho del objeto más que su dirección de memoria, lo cual no es muy
relevante. Modifiquemos eso definiendo el método __repr__() debajo del inicializador.

1. def __repr__(self):

2. return f"<Time {self.h:02}:{self.m:02}:{self.s:02}>"

(Si tienes dudas sobre la sintaxis empleada aquí para constituir la cadena véase Formando
cadenas de caracteres).

El resultado de este método será retornado por la función str() al intentar convertir una instancia de
nuestra clase a una cadena, que es justamente lo que hace print(). Por ello, si ahora imprimimos
nuestro objeto, veremos una representación más clara.

1. print(a) # <Time 14:23:10>


¡Excelente! Ya implementamos nuestro primer método mágico. Volveremos sobre ellos más
adelante.

Propiedades

Las propiedades nos permiten encapsular atributos dentro de una clase. ¿Cuál es su propósito?
Bien, consideremos lo siguiente.

1. a = Time(14, 23, 10)

2. a.m = "Hello, world!"

3. print(a) # ValueError!

Aquí básicamente el problema es que no hay nada que evite que se asigne una cadena a un atributo
que se espera sea un número entero. Para solucionarlo vamos a emplear el decorador
incorporado property(), que nos permite controlar de qué forma se obtiene y se altere el valor de un
atributo. Lo haremos para los tres atributos que nos competen.

Empecemos por las horas. Lo primero que debemos hacer es definir una función que será llamada
por Python cuando se intente acceder al valor de nuestro atributo.

1. @property

2. def h(self):

3. return self._h

De modo que print(a.h) imprimirá el valor retornado por el método h(). Pero aún no hemos definido
el atributo _h, esto es, el valor que queremos encapsular para evitar que le sea asignado otro tipo
de dato que no sea un entero. Entonces, lo siguiente es definir otra función que será llamada
cuando se asigne un nuevo valor al atributo h.

1. @h.setter

2. def h(self, value):

3. self._h = value

Así, hacer a.h = 50 será equivalente a invocar a la función anterior. Esto nos permite establecer
restricciones en los valores asignados, por ejemplo, chequear su tipo de dato.

1. @h.setter

2. def h(self, value):

3. if not isinstance(value, int):

4. raise TypeError("An integer is required.")

5. self._h = value

Comprobemos, entonces, que ahora el siguiente código lanza un error.


1. a = Time(14, 23, 10)

2. a.h = "Hello, world!" # TypeError

Y lo mismo ocurrirá si los argumentos no son del tipo correcto, por cuanto el método __init__() se
ocupa de asignar los valores pasados a sus respectivos atributos.

1. a = Time("Hello, world!", 23, 10) # TypeError

Hagamos, ahora, esto mismo para los otros dos atributos, y creemos un decorador adicional que se
ocupe de chequear que siempre el argumento value sea un entero.

1. from functools import wraps

2.

3.

4. def _int_required(f):

5. @wraps(f)

6. def wrapper(self, value):

7. if not isinstance(value, int):

8. raise TypeError("An integer is required.")

9. return f(self, value)

10. return wrapper

11.

12.

13. class Time:

14. # [...]

15.

16. @property

17. def h(self):

18. return self._h

19.

20. @h.setter

21. @_int_required

22. def h(self, value):


23. self._h = value

24.

25. @property

26. def m(self):

27. return self._m

28.

29. @m.setter

30. @_int_required

31. def m(self, value):

32. self._m = value

33.

34. @property

35. def s(self):

36. return self._s

37.

38. @s.setter

39. @_int_required

40. def s(self, value):

41. self._s = value

¡Genial! Ya tenemos el problema del tipo de dato solucionado.

Ahora ocupémonos de este otro tema. Sabemos que 60 segundos equivalen a un minuto y que 60
minutos equivalen a una hora. Debemos asegurar que nuestra clase se encargue de hacer el
balance de los datos de forma automática: por ejemplo, convertir 80 segundos en 1 minuto y 20
segundos, asignando los atributos correspondientes. Para ello añadamos un balance en los
métodos invocados cuando se asigna a los atributos m y s (setters).

1. @m.setter

2. @_int_required

3. def m(self, value):

4. self._m = value

5. self._h, self._m = _balance(self._h, self._m)


6.

7. # [...]

8.

9. @s.setter

10. @_int_required

11. def s(self, value):

12. self._s = value

13. self._m, self._s = _balance(self._m, self._s)

La función _balance() la crearemos antes de la definición de la clase.

1. def _balance(a, b):

2. if b >= 0:

3. while b >= 60:

4. a += 1

5. b -= 60

6. elif b < 0:

7. while b < 0:

8. a -= 1

9. b += 60

10. return a, b

Evito la explicación sobre cómo opera la función pues no es el tema central del artículo. Sí
comprobemos que ahora el balance ocurre en forma automática.

1. a = Time(2, 80, 95)

2. print(a) # <Time 03:21:35>

Operaciones aritméticas

Sería útil que podamos sumar y restar instancias de Time usando los operadores + y -. Python nos
permite implementarlo de una forma muy sencilla: definiendo los métodos __add__() y __sub__(),
respectivamente. De igual forma ─aunque no lo haremos aquí pues no nos sirve a nuestro
propósito─ están disponibles __mul__() y __truediv__() para la multiplicación (*) y la división (/).

Comencemos por la adición.

1. def __add__(self, other):


2. h = self.h + other.h

3. m = self.m + other.m

4. s = self.s + other.s

5. return Time(h, m, s)

La lógica es bastante fácil: si a y b son dos instancias de Time, al hacer a + b Python devolverá el
resultado de a.__add__(b).

1. a = Time(2, 16, 48)

2. b = Time(3, 51, 22)

3. print(a + b) # <Time 06:08:10>

Para la resta el procedimiento es similar.

1. def __sub__(self, other):

2. h = self.h - other.h

3. m = self.m - other.m

4. s = self.s - other.s

5. return Time(h, m, s)

También podemos expresarlo de una forma más funcional del siguiente modo.

1. import operator

2.

3. # [...]

4.

5. def _operation(self, other, method):

6. h = method(self.h, other.h)

7. m = method(self.m, other.m)

8. s = method(self.s, other.s)

9. return Time(h, m, s)

10.

11. def __add__(self, other):

12. return self._operation(other, operator.add)

13.
14. def __sub__(self, other):

15. return self._operation(other, operator.sub)

(El módulo estándar operator define funciones que actúan del mismo modo que los operadores de
Python, por ejemplo, operator.add(a, b) es igual a a + b).

Ahora bien, para evitar que se intente sumar o restar una instancia de Time con un objeto de
cualquier otro tipo, debemos chequear el tipo de dato del argumento. Creemos un decorador para
ello.

1. def _time_required(f):

2. @wraps(f)

3. def wrapper(self, other):

4. if not isinstance(other, Time):

5. raise TypeError("Can only operate on Time objects.")

6. return f(self, other)

7. return wrapper

Y apliquémoslo a los dos métodos que acabamos de crear.

1. @_time_required

2. def __add__(self, other):

3. return self._operation(other, operator.add)

4.

5. @_time_required

6. def __sub__(self, other):

7. return self._operation(other, operator.sub)

Ejemplo:

1. a = Time(2, 16, 48)

2. print(a + 10) # TypeError

¡Perfecto! Veamos a continuación cómo permitir comparaciones entre dos instancias de nuestra
clase.

Comparaciones

Los métodos mágicos para cada una de las operaciones de comparación son los siguientes.

• __lt__() para a < b.


• __gt__() para a > b.

• __le__() para a <= b.

• __ge__() para a >= b.

• __ne__() para a != b.

• __eq__() para a == b.

Implementemos primero el último, que es más sencillo. Sabemos que una instancia de Time será
igual a otra cuando coincidan las horas, los minutos y los segundos.

1. @_time_required

2. def __eq__(self, other):

3. return (self.h == other.h and

4. self.m == other.m and

5. self.s == other.s)

Algunas pruebas:

1. print(Time(2, 16, 48) == Time(2, 16, 48)) # True

2. print(Time(2, 16, 48) == Time(8, 16, 21)) # False

¡Sencillo! Ahora sigamos con la operación a < b. En este caso, a será menor a b si a.h < b.h. Si las
horas son iguales, debemos chequear los minutos. Y si estos son iguales, los segundos.

1. @_time_required

2. def __lt__(self, other):

3. if self.h < other.h:

4. return True

5. if self.h > other.h:

6. return False

7. if self.m < other.m:

8. return True

9. if self.m > other.m:

10. return False

11. return self.s < other.s

Otras pruebas:
1. print(Time(2, 16, 48) < Time(2, 16, 48)) # False

2. print(Time(2, 16, 48) < Time(8, 16, 21)) # True

Bien. Podríamos definir el resto de los métodos de comparación tal como hemos hecho con estos.
Pero dado que las otras operaciones pueden inferirse a partir de estas dos que definimos (por
ejemplo, a != b es igual a not a == b), no es necesario que lo hagamos manualmente. El
decorador functools.total_ordering() se encargará de ello.

1. from functools import total_ordering, wraps

2.

3. # [...]

4.

5. @total_ordering

6. class Time:

7.

8. # [...]

¡Listo! Ya con eso tenemos la definición de todos los operadores de comparación.

1. a = Time(2, 16, 48)

2. b = Time(8, 16, 21)

3. print(a == b) # False

4. print(a != b) # True

5. print(a > b) # False

6. print(a < b) # True

7. print(a >= b) # False

8. print(a <= b) # True

Código completo

¡Hemos llegado al final! Muchos otros son los métodos mágicos disponibles en Python; la lista
completa está en la documentación oficial. A continuación el código al que hemos arribado a lo
largo del artículo.

1. #!/usr/bin/env python

2. # -*- coding: utf-8 -*-

3.
4. import operator

5. from functools import total_ordering, wraps

6.

7.

8. def _time_required(f):

9. @wraps(f)

10. def wrapper(self, other):

11. if not isinstance(other, Time):

12. raise TypeError("Can only operate on Time objects.")

13. return f(self, other)

14. return wrapper

15.

16.

17. def _int_required(f):

18. @wraps(f)

19. def wrapper(self, value):

20. if not isinstance(value, int):

21. raise TypeError("An integer is required.")

22. return f(self, value)

23. return wrapper

24.

25.

26. def _balance(a, b):

27. if b >= 0:

28. while b >= 60:

29. a += 1

30. b -= 60

31. elif b < 0:

32. while b < 0:


33. a -= 1

34. b += 60

35. return a, b

36.

37.

38. @total_ordering

39. class Time:

40.

41. def __init__(self, h=0, m=0, s=0):

42. self.h = h

43. self.m = m

44. self.s = s

45.

46. @property

47. def h(self):

48. return self._h

49.

50. @h.setter

51. @_int_required

52. def h(self, value):

53. self._h = value

54.

55. @property

56. def m(self):

57. return self._m

58.

59. @m.setter

60. @_int_required

61. def m(self, value):


62. self._m = value

63. self._h, self._m = _balance(self._h, self._m)

64.

65. @property

66. def s(self):

67. return self._s

68.

69. @s.setter

70. @_int_required

71. def s(self, value):

72. self._s = value

73. self._m, self._s = _balance(self._m, self._s)

74.

75. def _operation(self, other, method):

76. h = method(self.h, other.h)

77. m = method(self.m, other.m)

78. s = method(self.s, other.s)

79. return Time(h, m, s)

80.

81. @_time_required

82. def __add__(self, other):

83. return self._operation(other, operator.add)

84.

85. @_time_required

86. def __sub__(self, other):

87. return self._operation(other, operator.sub)

88.

89. @_time_required

90. def __eq__(self, other):


91. return (self.h == other.h and

92. self.m == other.m and

93. self.s == other.s)

94.

95. @_time_required

96. def __lt__(self, other):

97. if self.h < other.h:

98. return True

99. if self.h > other.h:

100. return False

101. if self.m < other.m:

102. return True

103. if self.m > other.m:

104. return False

105. return self.s < other.s

106.

107. def __repr__(self):

108. return f"<Time {self.h:02}:{self.m:02}:{self.s:02}>"

Artículos relacionados

• Clases y orientación a objetos

• Booleanos, operaciones lógicas y binarias

• Iteradores, iterables y la función next()

• functools - Operaciones con funciones

• Cómo empezar con Python

• Cómo usar la función super() eficientemente

Curso online �

¡Ya lanzamos el curso oficial de Recursos Python en Udemy! Un curso moderno para aprender
Python desde cero con programación orientada a objetos, SQL y tkinter en 2024.

Consultoría �
Ofrecemos servicios profesionales de desarrollo y capacitación en Python a personas y
empresas. Consultanos por tu proyecto.

Interfaces y Abstract Base Class (ABC)

En la programación orientada a objetos, un interfaz define al conjunto de métodos que tiene que
tener un objeto para que pueda cumplir una determinada función en nuestro sistema. Dicho de otra
manera, un interfaz define como se comporta un objeto y lo que se puede hacer con el.

Piensa en el mando a distancia del televisor. Todos los mandos nos ofrecen el mismo interfaz con
las mismas funcionalidades o métodos. En pseudocódigo se podría escribir su interfaz como:

# Pseudocódigo

interface Mando{

def siguiente_canal():

def canal_anterior():

def subir_volumen():

def bajar_volumen():

Es importante notar que los interfaces no poseen una implementación per se, es decir, no llevan
código asociado. El interfaz se centra en el qué y no en el cómo.

Se dice entonces que una determinada clase implementa una interfaz, cuando añade código a los
métodos que no lo tenían (denominados abstractos). Es decir, implementar un interfaz consiste en
pasar del qué se hace al cómo se hace.

Podríamos decir entonces que los mandos de Samsung y LG implementan nuestro interfaz Mando,
ya que ambos tienen los métodos definidos, pero con implementaciones diferentes. Esto es debido
a que cada empresa resuelve el mismo problema con un enfoque diferente, pero lo que se ofrece
visto desde el exterior es lo mismo.

Aunque lo veremos más adelante, ya podemos adelantar que Python no posee


la keyword interface como otros lenguajes de programación. A pesar de esto, existen dos formas de
definir interfaces en Python:

• Interfaces informales

• Interfaces formales

Dependiendo de la magnitud y tipo del proyecto en el que trabajemos, es posible que los interfaces
informales sean suficientes. Sin embargo, a veces no bastan, y es donde entran los interfaces
formales y las metaclases, ambos conceptos bastante avanzados pero que la mayoría de
programadores tal vez pueda ignorar.

Interfaces informales
Los interfaces informales pueden ser definidos con una simple clase que no implementa los
métodos. Volviendo al ejemplo de nuestro interfaz mando a distancia, lo podríamos escribir en
Python como:

class Mando:

def siguiente_canal(self):

pass

def canal_anterior(self):

pass

def subir_volumen(self):

pass

def bajar_volumen(self):

pass

Una vez definido nuestro interfaz informal, podemos usarlo mediante herencia. Las
clases MandoSamsung y MandoLG implementan el interfaz Mando con código particular en los
métodos. Recuerda, pasamos del qué hace al cómo se hace.

class MandoSamsung(Mando):

def siguiente_canal(self):

print("Samsung->Siguiente")

def canal_anterior(self):

print("Samsung->Anterior")

def subir_volumen(self):

print("Samsung->Subir")

def bajar_volumen(self):

print("Samsung->Bajar")

Análogamente creamos MandoLG.

class MandoLG(Mando):

def siguiente_canal(self):

print("LG->Siguiente")

def canal_anterior(self):

print("LG->Anterior")
def subir_volumen(self):

print("LG->Subir")

def bajar_volumen(self):

print("LG->Bajar")

Como hemos dicho, esto es una solución perfectamente válida en la mayoría de los casos, pero
existe un problema con el que entenderás perfectamente porqué lo llamamos interfaz informal.

Al heredar de la clase Mando, no se obliga a MandoSamsung o MandoLG a implementar todos los


métodos. Es decir, ambas clases podrían no tener código para todos los métodos, y esto es algo
que puede causar problemas.

El razonamiento es el siguiente. Si Mando es un interfaz que como tal no implementa ningún


método (tan sólo define los métodos), ¿no sería acaso importante asegurarse de que las clases que
usan dicho interfaz implementan los métodos?

Si un método queda sin implementar, podríamos tener problemas en el futuro, ya que al llamar a
dicho método no tendríamos código que ejecutar. Es cierto que se podría resolver
cambiando pass por raise NotImplementedError(), pero el error lo obtendríamos en tiempo de
ejecución.

Hasta aquí los interfaces informales. Nótese que este tipo de interfaces es posible en Python
debido a una de sus características estrella, el duck typing, por lo que te recomendamos que leas
acerca de este concepto tan importante en Python.

Interfaces formales

Una vez tenemos el contexto de lo que son los interfaces informales, ya estamos en condiciones de
entender los interfaces formales.

Los interfaces formales pueden ser definidos en Python utilizando el módulo por defecto llamado
ABC (Abstract Base Classes). Los abc fueron añadidos a Python en la PEP3119.

Simplemente definen una forma de crear interfaces (a través de metaclases) en los que se definen
unos métodos (pero no se implementan) y donde se fuerza a las clases que usan ese interfaz a
implementar los métodos. Veamos unos ejemplos.

El interfaz más sencillo que podemos crear es de la siguiente manera, heredando de abc.ABC.

from abc import ABC

class Mando(ABC):

pass

La siguiente sintaxis es también válida, y aunque se sale del contenido de este capítulo, es
importante que asocies el módulo abc con las metaclases.

from abc import ABCMeta


class Mando(metaclass=ABCMeta):

pass

Pero veamos un ejemplo concreto continuando con nuestro ejemplo del mando a distancia.
Podemos observar como se usa el decorador @abstractmethod.

Un método abstracto es un método que no tiene una implementación, es decir, que no lleva código.
Un método definido con este decorador, forzará a las clases que implementen dicho interfaz a
codificarlo.

Veamos como queda nuestro interfaz formal Mando.

from abc import abstractmethod

from abc import ABCMeta

class Mando(metaclass=ABCMeta):

@abstractmethod

def siguiente_canal(self):

pass

@abstractmethod

def canal_anterior(self):

pass

@abstractmethod

def subir_volumen(self):

pass

@abstractmethod

def bajar_volumen(self):

pass

Lo primero a tener en cuenta es que no se puede crear un objeto de una clase interfaz, ya que sus
métodos no están implementados.

mando = Mando()
# TypeError: Can't instantiate abstract class Mando with abstract methods bajar_volumen,
canal_anterior, siguiente_canal, subir_volumen

Sin embargo si que podemos heredar de Mando para crear una clase MandoSamsung. Es muy
importante que implementemos todos los métodos, o de lo contrario tendremos un error. Esta es
una de las diferencias con respecto a los interfaces informales.

class MandoSamsung(Mando):

def siguiente_canal(self):

print("Samsung->Siguiente")

def canal_anterior(self):

print("Samsung->Anterior")

def subir_volumen(self):

print("Samsung->Subir")

def bajar_volumen(self):

print("Samsung->Bajar")

Y como de costumbre podemos crear un objeto y llamar a sus métodos.

mando_samsung = MandoSamsung()

mando_samsung.bajar_volumen()

# Samsung->Bajar

Siguiendo con el ejemplo podemos definir la clase MandoLG.

class MandoLG(Mando):

def siguiente_canal(self):

print("LG->Siguiente")

def canal_anterior(self):

print("LG->Anterior")

def subir_volumen(self):

print("LG->Subir")

def bajar_volumen(self):

print("LG->Bajar")

Y creamos un objeto de MandoLG.

mando_lg = MandoLG()
mando_lg.bajar_volumen()

# LG->Bajar

Llegados a este punto tenemos por lo tanto dos conceptos diferentes claramente identificados:

• Por un lado tenemos nuestro interfaz Mando. Se trata de una clase que define el
comportamiento de un mando genérico, pero sin centrarse en los detalles de cómo
funciona. Se centra en el qué.

• Por otro lado tenemos dos clases MandoSamsung y MandoLG que implementan/heredan el
interfaz anterior, añadiendo un código concreto y diferente para cada mando. Ambas clases
representan el cómo.

Hasta aquí hemos visto como crear un interfaz formal sencilla usando abc con métodos abstractos,
pero existen más funcionalidades que merece la pena ver. Vamos a por ello.

Clases virtuales

Como ya sabemos, se considera que una clase es subclase o issubclass de otra si hereda de la
misma, como podemos ver en el siguiente ejemplo.

class ClaseA:

pass

class ClaseB(ClaseA):

pass

print(issubclass(ClaseB, ClaseA))

# True

Pero, ¿y si queremos que se considere a una clase la padre cuando no existe herencia entre ellas?

Es aquí donde entran las clases virtuales. Usando register() podemos registrar a una ABC como
clase padre de otra. En el siguiente ejemplo FloatABC se registra como clase virtual padre de float.

from abc import ABCMeta

class FloatABC(metaclass=ABCMeta):

pass

FloatABC.register(float)

Y esto implica que el comportamiento de issubclass se ve modificado.


print(issubclass(float, FloatABC))

# True

Análogamente podemos realizar lo mismo con una clase definida por nosotros.

@FloatABC.register

class MiFloat():

pass

x = MiFloat()

print(issubclass(MiFloat, FloatABC))

# True

Métodos abstractos

Como ya hemos visto los métodos abstractos son aquellos que son declarados pero no tienen una
implementación. También hemos visto como Python nos obliga a implementarlos en la clases que
heredan de nuestro interfaz. Esto es posible gracias al decorador @abstractmethod.

from abc import ABC, abstractmethod

class Clase(metaclass=ABCMeta):

@abstractmethod

def metodo_abstracto(self):

pass

Sin embargo, también es posible combinar el decorador @abstractmethod con los


decoradores @classmethod y @staticmethod que ya vimos anteriormente. Nótese
que @abstractmethod debe ser usado siempre justo antes del método.

Como recordatorio, un método de clase es llamado sobre la clase y no sobre el objeto, pudiendo
modificar la clase pero no el objeto.

class Clase(ABC):

@classmethod

@abstractmethod

def metodo_abstracto_de_clase(cls):

pass

Análogamente podemos definir un @staticmethod. Se trata de un método que no permite realizar


modificaciones ni de la clase ni del objeto, ya que no lo recibe como parámetro.
class Clase(ABC):

@staticmethod

@abstractmethod

def metodo_abstracto_estatico():

pass

Y por último también podemos combinarlo con el decorador property.

class Clase(ABC):

@property

@abstractmethod

def metodo_abstracto_propiedad(self):

pass

Abstract Base Classes y colecciones

Python nos ofrece un conjunto de Abstract Base Classes que podemos usar para crear nuestras
propias clases, denominado collections.abc. Es por tanto importante echarles un vistazo, ya que
tal vez exista ya la que necesitemos.

Podemos por ejemplo crear una clase MiSet que use abc.Set, pero que tenga un comportamiento
ligeramente distinto. En este caso, deberemos implementar los métodos
mágicos __iter__, __contains__ y __len__, ya que son definidos como abstractos en el abc.

from collections import abc

class MiSet(abc.Set):

def __init__(self, iterable):

self.elements = []

for value in iterable:

if value not in self.elements:

self.elements.append(value)

def __iter__(self):

return iter(self.elements)

def __contains__(self, value):


return value in self.elements

def __len__(self):

return len(self.elements)

def __str__(self):

return "".join(str(i) for i in self.elements)

Como podemos ver, heredamos ciertas funcionalidades como los operadores & y | que pueden ser
usados sobre nuestra nueva clase.

s1 = MiSet("abcdefg")

s2 = MiSet("efghij")

print(s1 & s2)

print(s1 | s2)

# efg

# abcdefghij

Abstracción en programación

La abstracción es un termino que hace referencia a la ocultación de la complejidad intrínseca de


una aplicación al exterior, centrándose sólo en como puede ser usada, lo que se conoce
como interfaz. Si has oído hablar del enfoque caja negra, es un concepto muy relacionado. Dicho
en otras palabras, la abstracción consiste en ocultar toda la complejidad que algo puede tener por
dentro, ofreciéndonos unas funciones de alto nivel, por lo general sencillas de usar, que pueden ser
usadas para interactuar con la aplicación sin tener conocimiento de lo que hay dentro.

Una analogía del mundo real podría ser la televisión. Se trata de un dispositivo muy complejo
donde han trabajado gran cantidad de ingenieros de diversas disciplinas como telecomunicaciones
o electrónica. ¿Os imagináis que para cambiar de canal tuviéramos que saber todos los entresijos
del aparato?. Pues bien, se nos ofrece una abstracción de la televisión, un mando a distancia. El
mando nos abstrae por completo de la complejidad de los circuitos y señales, y nos da una interfaz
sencilla que con unos pocos botones podemos usar.

Algo muy parecido sucede en la programación orientada a objetos. Si tuviéramos una


clase Televisor, en su interior podría haber líneas y líneas de código super complejas, pero una
buena abstracción sería la que simplemente ofreciera los
métodos encender(), apagar() y cambiar_canal() al exterior.
Un concepto relacionado con la abstracción, serían las clases abstractas o más bien los métodos
abstractos. Se define como clase abstracta la que contiene métodos abstractos, y se define como
método abstracto a un método que ha sido declarado pero no implementado. Es decir, que no tiene
código.

Es posible crear métodos abstractos en Python con decoradores como @absttractmethod, pero
esto lo dejamos para otro post.

Existen otros conceptos muy importantes y relacionados con la programación orientada a objetos.
Aquí te los dejamos:

• Herencia

• Cohesión

• Abstracción

• Polimorfismo

• Acoplamiento

• Encapsulamiento

Acoplamiento en programación

El acoplamiento en programación (denominado coupling en Inglés) es un concepto que mide la


dependencia entre dos módulos distintos de software, como pueden ser por ejemplo las clases. El
acoplamiento puede ser de dos tipos:

• Acoplamiento débil, que indica que no existe dependencia de un módulo con otros. Esto
debería ser la meta de nuestro software.

• Acoplamiento fuerte, que por lo contrario indica que un módulo tiene dependencias internas
con otros.

El término acoplamiento está muy relacionado con la cohesión, ya que acoplamiento débil suele ir
ligado a cohesión fuerte. En general lo que buscamos en nuestro código es que tenga acoplamiento
débil y cohesión fuerte, es decir, que no tenga dependencias con otros módulos y que las tareas
que realiza estén relacionadas entre sí. Un código así es fácil de leer, de reusar, mantener y tiene
que ser nuestra meta. Nótese que se suele emplear alta y baja para designar fuerza y débil
respectivamente.

Si aún no te hemos convencido de porque buscamos código débilmente acoplado, veamos lo que
pasaría con un código fuertemente acoplado:

• Debido a las dependencias con otros módulo, un cambio en un modulo ajeno al nuestro
podría tener un “efecto mariposa” en nuestro código, aún sin haber modificado
directamente nuestro módulo.

• Si un módulo tiene dependencias con otros, reduce la reusabilidad, ya que para reusarlo
deberíamos copiar también las dependencias.
Veamos un ejemplo usando clases y objetos en Python. Tenemos una Clase1 que define un atributo
de clase x. Por otro lado la Clase2 basa el comportamiento del método mi_metodo() en el valor
de x de la Clase1. En este ejemplo existe acoplamiento fuerte, ya que existe una dependencia con
una variable de otro módulo.

class Clase1:

x = True

pass

class Clase2:

def mi_metodo(self, valor):

if Clase1.x:

self.valor = valor

mi_clase = Clase2()

mi_clase.mi_metodo("Hola")

mi_clase.valor

Puede parecer un ejemplo trivial, pero cuando el software se va complicando, no es nada raro
acabar haciendo cosas de este tipo casi sin darnos cuenta. Hay veces que dependencias externas
pueden estar justificadas, pero hay que estar muy seguro de lo que se hace.

Este tipo de dependencias también puede hacer el código muy difícil de depurar. Imaginemos que
nuestro código de la Clase2 funciona perfectamente, pero de repente alguien hace un cambio en
la Clase1. Un cambio tan sencillo como el siguiente.

Clase1.x = False

Este cambio estaría modificando el comportamiento de nuestra clase y nos preguntaríamos


¿porqué ha dejado de funcionar mi código si no he tocado nada? A veces atribuimos estos
comportamientos a la magia o radiación cósmica, pero simplemente tenemos código con
acoplamiento fuerte.

Existen otros conceptos muy importantes y relacionados con la programación orientada a objetos.
Aquí te los dejamos:

• Herencia

• Cohesión

• Abstracción
• Polimorfismo

• Acoplamiento

• Encapsulamiento

Crear clase Python

Crear una clase en Python se puede hacer un tan sólo dos líneas de código haciendo uso de la
palabra class.

class MiClase:

pass

Añadir atributos a la clase

Podemos añadir algún atributo de clase. En este caso tenemos atributos generales que pertenecen
a la clase y no a la instancia.

class MiClase:

atributo1 = "valor1"

atributo2 = "valor2"

Añadir constructor a la clase

Se podría decir que toda clase tiene un constructor, que recibe unos parámetros de entrada cuando
el objeto es creado. Creamos por lo tanto el constructor __init__. Nótese la diferencia
entre atributo1 y argumento1 (pista, atributo de clase vs instancia).

class MiClase:

atributo1 = "valor1"

atributo2 = "valor2"

def __init__(self, argumento1):

self.argumento1 = argumento1

Ya vamos teniendo una clase mucho más completa, pero sigamos.

Añadir métodos a la clase

A parte de atributos y el constructor, toda clase tiene un conjunto de funciones o métodos que
realizan diferentes funcionalidades. Creamos la funcion1().

class MiClase:

atributo1 = "valor1"

atributo2 = "valor2"
def __init__(self, argumento1):

self.argumento1 = argumento1

def funcion1(self):

print("Esta es la función 1")

Crear objeto

A diferencia de la clase, un objeto define una clase particular, con unos atributos particulares para
ese objeto. Es decir, el objeto es la instancia de la clase. Se puede crear usando () sobre la clase y
pasando los argumentos de entrada separados por ,.

mi_clase = MiClase("Hola")

Acceder a métodos y atributos

Una vez tenemos el objeto mi_clase, podemos acceder a todo su contenido, tanto métodos como
atributos de clase o de instancia. Simplemente hay que usar el objeto . y el método o atributo.

mi_clase.atributo1

mi_clase.atributo2

mi_clase.argumento1

mi_clase.funcion1()

Para más detalle, te dejamos otros posts por si te pudieran interesar:

• Programación orientada a objetos, una introducción a clases, objetos, métodos y atributos

• Métodos «normales», estáticos y de clase en Python. Diferencias y usos

• Dando un toque de elegancia con el decorador @property en nuestras clases

Encapsulamiento en programación

El encapsulamiento o encapsulación en programación es un concepto relacionado con la


programación orientada a objetos, y hace referencia al ocultamiento de los estado internos de una
clase al exterior. Dicho de otra manera, encapsular consiste en hacer que los atributos o métodos
internos a una clase no se puedan acceder ni modificar desde fuera, sino que tan solo el propio
objeto pueda acceder a ellos.

Para la gente que conozca Java, le resultará un termino muy familiar, pero en Python es algo
distinto. Digamos que Python por defecto no oculta los atributos y métodos de una clase al exterior.
Veamos un ejemplo con el lenguaje Python.

class Clase:

atributo_clase = "Hola"

def __init__(self, atributo_instancia):


self.atributo_instancia = atributo_instancia

mi_clase = Clase("Que tal")

mi_clase.atributo_clase

mi_clase.atributo_instancia

# 'Hola'

# 'Que tal'

Ambos atributos son perfectamente accesibles desde el exterior. Sin embargo esto es algo que tal
vez no queramos. Hay ciertos métodos o atributos que queremos que pertenezcan sólo a la clase o
al objeto, y que sólo puedan ser accedidos por los mismos. Para ello podemos usar la doble __ para
nombrar a un atributo o método. Esto hará que Python los interprete como “privados”, de manera
que no podrán ser accedidos desde el exterior.

class Clase:

atributo_clase = "Hola" # Accesible desde el exterior

__atributo_clase = "Hola" # No accesible

# No accesible desde el exterior

def __mi_metodo(self):

print("Haz algo")

self.__variable = 0

# Accesible desde el exterior

def metodo_normal(self):

# El método si es accesible desde el interior

self.__mi_metodo()

mi_clase = Clase()

#mi_clase.__atributo_clase # Error! El atributo no es accesible

#mi_clase.__mi_metodo() # Error! El método no es accesible


mi_clase.atributo_clase # Ok!

mi_clase.metodo_normal() # Ok!

Y como curiosidad, podemos hacer uso de dir para ver el listado de métodos y atributos de nuestra
clase. Podemos ver claramente como tenemos el metodo_normal y el atributo de clase, pero no
podemos encontrar __mi_metodo ni __atributo_clase.

print(dir(mi_clase))

#['_Clase__atributo_clase', '_Clase__mi_metodo', '_Clase__variable',

#'__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__',

#'__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__',

#'__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__',

#'__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__',

#'__str__', '__subclasshook__', '__weakref__', 'atributo_clase', 'metodo_normal']

Pues bien, en realidad si que podríamos acceder a __atributo_clase y a __mi_metodo haciendo un


poco de trampa. Aunque no se vea a simple vista, si que están pero con un nombre distinto, para de
alguna manera ocultarlos y evitar su uso. Pero podemos llamarlos de la siguiente manera, pero por
lo general no es una buena idea.

mi_clase._Clase__atributo_clase

# 'Hola'

mi_clase._Clase__mi_metodo()

# 'Haz algo'

Existen otros conceptos muy importantes y relacionados con la programación orientada a objetos.
Aquí te los dejamos:

• Herencia

• Cohesión

• Abstracción

• Polimorfismo

• Acoplamiento

• Encapsulamiento

Polimorfismo en programación

El polimorfismo es uno de los pilares básicos en la programación orientada a objetos, por lo que
para entenderlo es importante tener las bases de la POO y la herencia bien asentadas.
El término polimorfismo tiene origen en las palabras poly (muchos) y morfo (formas), y aplicado a la
programación hace referencia a que los objetos pueden tomar diferentes formas. ¿Pero qué
significa esto?

Pues bien, significa que objetos de diferentes clases pueden ser accedidos utilizando el mismo
interfaz, mostrando un comportamiento distinto (tomando diferentes formas) según cómo sean
accedidos.

En lenguajes de programación como Python, que tiene tipado dinámico, el polimorfismo va muy
relacionado con el duck typing.

Sin embargo, para entender bien este concepto, es conveniente explicarlo desde el punto de vista
de un lenguaje de programación con tipado estático como Java. Vamos a por ello.

Polimorfismo en Java

Vamos a comenzar definiendo una clase Animal.

// Código Java

class Animal{

public Animal() {}

Y dos clases Perro y Gato que heredan de la anterior.

// Código Java

class Perro extends Animal {

public Perro() {}

class Gato extends Animal {

public Gato() {}

El polimorfismo es precisamente lo que nos permite hacer lo siguiente:

// Código Java

Animal a = new Perro();

Recuerda que Java es un lenguaje con tipado estático, lo que significa que el tipo tiene que ser
definido al crear la variable.
Sin embargo estamos asignando a una variable Animal un objeto de la clase Perro. ¿Cómo es esto
posible?

Pues ahí lo tienes, el polimorfismo es lo que nos permite usar ambas clases de forma indistinta, ya
que soportan el mismo “interfaz” (no confundir con el interface de Java).

El siguiente código es también correcto. Tenemos un array de Animal donde cada elemento toma la
forma de Perro o de Gato.

// Código Java

Animal[] animales = new Animal[10];

animales[0] = new Perro();

animales[1] = new Gato();

Sin embargo, no es posible realizar lo siguiente, ya que OtraClase no comparte interfaz con Animal.
Tendremos un error error: incompatible types.

// Código Java

class OtraClase {

public OtraClase() {}

Animal a = new OtraClase();

animales[0] = new OtraClase();

Polimorfismo en Python

El término polimorfismo visto desde el punto de vista de Python es complicado de explicar sin
hablar del duck typing, por lo que te recomendamos la lectura.

Al ser un lenguaje con tipado dinámico y permitir duck typing, en Python no es necesario que los
objetos compartan un interfaz, simplemente basta con que tengan los métodos que se quieren
llamar.

Podemos recrear el ejemplo de Java de la siguiente manera. Supongamos que tenemos un


clase Animal con un método hablar().

class Animal:

def hablar(self):

pass

Por otro lado tenemos otras dos clases, Perro, Gato que heredan de la anterior. Además,
implementan el método hablar() de una forma distinta.

class Perro(Animal):
def hablar(self):

print("Guau!")

class Gato(Animal):

def hablar(self):

print("Miau!")

A continuación creamos un objeto de cada clase y llamamos al método hablar(). Podemos observar
que cada animal se comporta de manera distinta al usar hablar().

for animal in Perro(), Gato():

animal.hablar()

# Guau!

# Miau!

En el caso anterior, la variable animal ha ido “tomando las formas” de Perro y Gato. Sin embargo,
nótese que al tener tipado dinámico este ejemplo hubiera funcionado igual sin que existiera
herencia entre Perro y Gato, pero esta explicación la dejamos para el capítulo de duck typing

Existen otros conceptos muy importantes y relacionados con la programación orientada a objetos.
Aquí te los dejamos:

• Herencia

• Cohesión

• Abstracción

• Polimorfismo

• Acoplamiento

• Encapsulamiento

Cohesión en Programación

La cohesión hace referencia al grado de relación entre los elementos de un módulo. En el diseño de
una función, es importante pensar bien la tarea que va a realizar, intentando que sea única y bien
definida. Cuantas más cosas diferentes haga una función sin relación entre sí, más complicado
será el código de entender. Existen por lo tanto dos tipos de cohesión:

• Por un lado tenemos la cohesión débil que indica que la relación entre los elementos es
baja. Es decir, no pertenecen a una única funcionalidad.
• Por otro la cohesión fuerte, que debe ser nuestro objetivo al diseñar programas. La cohesión
fuerte indica que existe una alta relación entre los elementos existentes dentro del módulo.

Existe también otro concepto llamado acoplamiento que explicamos en otro post. Normalmente
acoplamiento débil se relaciona con cohesión fuerte o alta.

Veámoslo con un ejemplo. Tenemos una función suma() que suma dos números. El problema es
que además de sumar dos números, los convierte a float() y además pide al usuario que introduzca
por pantalla el número. Podría parecer que esas otras dos funcionalidades no son para tanto, pero
si por ejemplo una persona quiere usar nuestra función suma() pero ya tiene los números y no
quiere pedirlos por pantalla, no le serviría nuestra función.

# Mal. Cohesión débil

def suma():

num1 = float(input("Dame primer número"))

num2 = float(input("Dame segundo número"))

suma = num1 + num2

print(suma)

suma()

Para que la función tuviese una cohesión fuerte, sería conveniente que la suma realizara una única
tarea bien definida, que es sumar.

# Bien. Cohesión fuerte

def suma(numeros):

total = 0

for i in numeros:

total = total + i

return total

Evidentemente un ejemplo tan sencillo como el explicado no tiene implicaciones demasiado


graves, pero es importante buscar que las funciones realicen una única tarea (o conjunto) pero
relacionadas entre sí. Diseñar código con cohesión fuerte nos permite:

• Reducir la complejidad del módulo, ya que tendrá un menor número de operaciones.

• Se podrá reutilizar los módulos más fácilmente

• El sistema será más fácilmente mantenible.


Existen otros conceptos muy importantes y relacionados con la programación orientada a objetos.
Aquí te los dejamos:

• Herencia

• Cohesión

• Abstracción

• Polimorfismo

• Acoplamiento

• Encapsulamiento

Excepciones

• 📗📗 Excepciones en Python

• 📗📗 Uso del assert()

• 📙📙 Definiendo Excepciones

• 📕📕 Context Managers

Excepciones en Python

Las excepciones en Python son una herramienta muy potente que la gran mayoría de lenguajes de
programación modernos tienen. Se trata de una forma de controlar el comportamiento de un
programa cuando se produce un error.

Esto es muy importante ya que salvo que tratemos este error, el programa se parará, y esto es algo
que en determinadas aplicaciones no es una opción válida.

Imaginemos que tenemos el siguiente código con dos variables a y b y realizamos su división a/b.

a=4

b=2

c = a/b

Pero imaginemos ahora que por cualquier motivo las variables tienen otro valor, y que por
ejemplo b tiene el valor 0. Si intentamos hacer la división entre cero, este programa dará un error y
su ejecución terminará de manera abrupta.

a = 4; b = 0

print(a/b)

# ZeroDivisionError: division by zero


Ese “error” que decimos que ha ocurrido es lanzado por Python (raise en Inglés) ya que la división
entre cero es una operación que matemáticamente no está definida.

Se trata de la excepción ZeroDivisionError. En el siguiente enlace, tenemos un listado de todas las


excepciones con las que nos podemos encontrar.

Veamos un ejemplo con otra excepción. ¿Que pasaría si intentásemos sumar un número con un
texto? Evidentemente esto no tiene ningún sentido, y Python define una excepción para esto
llamada TypeError.

print(2 + "2")

En base a esto es muy importante controlar las excepciones, porque por muchas comprobaciones
que realicemos es posible que en algún momento ocurra una, y si no se hace nada el programa se
parará.

¿Te imaginas que en un avión, un tren o un cajero automático tiene un error que lanza raise una
excepción y se detiene por completo?

Una primera aproximación al control de excepciones podría ser el siguiente ejemplo. Podemos
realizar una comprobación manual de que no estamos dividiendo por cero, para así evitar tener un
error tipo ZeroDivisionError.

Sin embargo es complicado escribir código que contemple y que prevenga todo tipo de
excepciones. Para ello, veremos más adelante el uso de except.

a=5

b=0

# A través de esta comprobación prevenimos que se divida entre cero.

if b!=0:

print(a/b)

else:

print("No se puede dividir!")

Uso de raise

También podemos ser nosotros los que levantemos o lancemos una excepción. Volviendo a los
ejemplos usados en el apartado anterior, podemos ser nosotros los que
levantemos ZeroDivisionError o NameError usando raise. La sintaxis es muy fácil.

raise ZeroDivisionError("Información de la excepción")

O podemos lanzar otra de tipo NameError.

raise NameError("Información de la excepción")


Se puede ver como el string que hemos pasado se imprime a continuación de la excepción. Se
puede llamar también sin ningún parámetro como se hace a continuación.

raise ZeroDivisionError

Visto esto, ya sabemos como una excepción puede ser lanzada. Existen dos maneras
principalmente:

• Hacemos una operación que no puede ser realizada (como dividir por cero). En este caso
Python se encarga de lanzar automáticamente la excepción.

• O también podemos lanzar nosotros una excepción manualmente, usando raise.

• Habría un tercer caso que sería lanzar una excepción que no pertenece a las definidas por
defecto en Python. Pero eso te lo explicamos aquí.

A continuación veremos que podemos hacer para controlar estas excepciones, y que hacer cuando
se lanza una para que no se interrumpa la ejecución del programa.

Uso de try y except

La buena noticia es que las excepciones que hemos visto antes, pueden ser capturadas y
manejadas adecuadamente, sin que el programa se detenga. Veamos un ejemplo con la división
entre cero.

a = 5; b = 0

try:

c = a/b

except ZeroDivisionError:

print("No se ha podido realizar la división")

En este caso no verificamos que b!=0. Directamente intentamos realizar la división y en el caso de
que se lance la excepción ZeroDivisionError, la capturamos y la tratamos adecuadamente.

La diferencia con el ejemplo anterior es que ahora no se para el programa y se puede seguir
ejecutando. Prueba a ejecutar el código y ver que pasa. Verás como el programa ya no se para.

Entonces, lo que hay dentro del try es la sección del código que podría lanzar la excepción que se
está capturando en el except. Por lo tanto cuando ocurra una excepción, se entra en
el except pero el programa no se para.

También se puede capturar diferentes excepciones como se ve en el siguiente ejemplo.

try:

#c = 5/0 # Si comentas esto entra en TypeError


d = 2 + "Hola" # Si comentas esto entra en ZeroDivisionError

except ZeroDivisionError:

print("No se puede dividir entre cero!")

except TypeError:

print("Problema de tipos!")

Puedes también hacer que un determinado número de excepciones se traten de la misma manera
con el mismo bloque de código. Sin embargo suele ser más interesante tratar a diferentes
excepciones de diference manera.

try:

#c = 5/0 # Si comentas esto entra en TypeError

d = 2 + "Hola" # Si comentas esto entra en ZeroDivisionError

except (ZeroDivisionError, TypeError):

print("Excepcion ZeroDivisionError/TypeError")

Otra forma si no sabes que excepción puede saltar, puedes usar la clase genérica Exception. En
este caso se controla cualquier tipo de excepción. De hecho todas las excepciones heredan
de Exception. Ver documentación.

try:

#c = 5/0 # Si comentas esto entra en TypeError

d = 2 + "Hola" # Si comentas esto entra en ZeroDivisionError

except Exception:

print("Ha habido una excepción")

No obstante hay una forma de saber que excepción ha sido la que ha ocurrido.

try:

d = 2 + "Hola" # Si comentas esto entra en ZeroDivisionError

except Exception as ex:

print("Ha habido una excepción", type(ex))

# Ha habido una excepción <class 'TypeError'>

Uso de else
Al ya explicado try y except le podemos añadir un bloque más, el else. Dicho bloque se ejecutará si
no ha ocurrido ninguna excepción. Fíjate en la diferencia entre los siguientes códigos.

try:

# Forzamos una excepción al dividir entre 0

x = 2/0

except:

print("Entra en except, ha ocurrido una excepción")

else:

print("Entra en else, no ha ocurrido ninguna excepción")

#Entra en except, ha ocurrido una excepción

Sin embargo en el siguiente código la división se puede realizar sin problema, por lo que el
bloque except no se ejecuta pero el else si es ejecutado.

try:

# La división puede realizarse sin problema

x = 2/2

except:

print("Entra en except, ha ocurrido una excepción")

else:

print("Entra en else, no ha ocurrido ninguna excepción")

#Entra en else, no ha ocurrido ninguna excepción

Uso de finally

A los ya vistos bloques try, except y else podemos añadir un bloque más, el finally. Dicho bloque se
ejecuta siempre, haya o no haya habido excepción.

Este bloque se suele usar si queremos ejecutar algún tipo de acción de limpieza. Si por ejemplo
estamos escribiendo datos en un fichero pero ocurre una excepción, tal vez queramos borrar el
contenido que hemos escrito con anterioridad, para no dejar datos inconsistenes en el fichero.
En el siguiente código vemos un ejemplo. Haya o no haya excepción el código que haya dentro
de finally será ejecutado.

try:

# Forzamos excepción

x = 2/0

except:

# Se entra ya que ha habido una excepción

print("Entra en except, ha ocurrido una excepción")

finally:

# También entra porque finally es ejecutado siempre

print("Entra en finally, se ejecuta el bloque finally")

#Entra en except, ha ocurrido una excepción

#Entra en finally, se ejecuta el bloque finally

Ejemplos

Un ejemplo muy típico de excepciones es en el manejo de ficheros. Se intenta abrir, pero se captura
una posible excepción. De hecho si entras en la documentación de open se indica que OSError es
lanzada si el fichero no puede ser abierto.

# Se intenta abrir un fichero y se captura una posible excepción

try:

with open('fichero.txt') as file:

read_data = file.read()

except:

# Se entra aquí si no pudo ser abierto

print('No se pudo abrir')

Como ya hemos comentado, en el except también se puede capturar una excepción concreta.
Dependiendo de nuestro programa, tal vez queramos tratar de manera distinta diferentes tipos de
excepciones, por lo que es una buena práctica especificar que tipo de excepción estamos tratando.

# Se intenta abrir un fichero y se captura una posible excepción

try:
with open('fichero.txt') as file:

read_data = file.read()

# Capturamos una excepción concreta

except OSError:

print('OSError. No se pudo abrir')

En este otro ejemplo vemos el uso de los bloques try, except, else y finally todos juntos.

try:

# Se fuerza excepción

x = 2/0

except:

print("Entra en except, ha ocurrido una excepción")

else:

print("Entra en el else, no ha ocurrido ninguna excepción")

finally:

print("Entra en finally, se ejecuta el bloque finally")

También se puede capturar una excepción de tipo SyntaxError, que hace referencia a errores de
sintaxis. Sin embargo el código debería estar libre de este tipo de fallos, por lo que tal vez nunca
deberías usar esto.

try:

print("Hola"))

except SyntaxError:

print("Hay un error de sintaxis")

Assert en Python

El uso de assert en Python nos permite realizar comprobaciones. Si la expresión contenida dentro
del mismo es False, se lanzará una excepción, concretamente AssertionError. Veamos un ejemplo:

assert(1==2)

# AssertionError

Es decir, si el contenido existente dentro del assert es igual a False, se lanzará la excepción. Se
podría conseguir el mismo resultado haciendo lo siguiente, pero el uso de assert() resulta más
cómodo.
if condicion:

raise AssertionError()

Podemos también añadir un texto con información relevante acerca del assert().

assert False, "El assert falló"

Aunque mucho cuidado, ya que la expresión anterior no es equivalente a la siguiente, siendo la


misma errónea. Esto se debe a que en realidad se está evaluando bool((False, "El assert falló")), lo
que resulta ser siempre True. De hecho el siguiente código no lanzaría una excepción, cuando
realmente se esperaría que lo hiciera.

# INCORRECTO

assert(False, "El assert falló")

Por otro lado, también se puede hacer uso del assert() sin usar paréntesis como se muestra a
continuación.

x = "ElLibroDePython"

assert x == "ElLibroDePython"

assert() en testing

La función assert() puede ser también muy útil para realizar testing de nuestro código,
especialmente para test unitarios o unit tests. Imagínate que tenemos una
función calcula_media() que como su nombre indica calcula la media de un conjunto de números.

def calcula_media(lista):

return sum(lista)/len(lista)

En el mundo de la programación es muy importante probar o testear el software, para asegurarse


de que está libre de errores. Gracias al uso de assert() podemos realizar estas comprobaciones de
manera automática.

assert(calcula_media([5, 10, 7.5]) == 7.5)

assert(calcula_media([4, 8]) == 6)

Por lo que si hacemos que estas comprobaciones sean parte de nuestro código, podríamos
proteger nuestra función, asegurándonos de que nadie la “rompa”.

assert() en funciones

Puede resultar útil usar assert() cuando queremos realizar alguna comprobación, como podría
ser dentro de una función. En el siguiente ejemplo tenemos una función suma() que sólo suma las
variables si son números enteros.

# Funcion suma de variables enteras


def suma(a, b):

assert(type(a) == int)

assert(type(b) == int)

return a+b

# Error, ya que las variables no son int

suma(3.0, 5.0)

# Ok, los argumentos son int

suma(3, 5)

assert() con clases

Otro ejemplo podría verificar que un objeto pertenece a una clase determinada.

class MiClase():

pass

class MiOtraClase():

pass

mi_objeto = MiClase()

mi_otro_objeto = MiOtraClase()

# Ok

assert(isinstance(mi_objeto, MiClase))

# Ok

assert(isinstance(mi_otro_objeto, MiOtraClase))

# Error, mi_objeto no pertenece a MiOtraClase

assert(isinstance(mi_objeto, MiOtraClase))
# Error, mi_otro_objeto no pertenece a MiClase

assert(isinstance(mi_otro_objeto, MiClase))

Deshabilitar assert

A modo de curiosidad, es posible ejecutar un script de Python deshabilitando el assert.


Supongamos que tenemos el siguiente código.

# ejemplo.py

assert(1==2)

Si ejecutamos nuestro script de la siguiente manera, los assert se eliminarán, por lo que no se
producirá ninguna excepción.

$ python -O ejemplo.py

Definiendo Excepciones

Antes de ver como se define una excepción, te recomendamos otros de nuestros posts que te
ayudarán a entenderlo mejor:

• Programación Orientada a Objetos

• Herencia en Python

• Excepciones en Python

En los posts anteriores verás como lanzar y capturar las excepciones. A continuación explicamos
como definirlas.

A pesar de que Python define un conjunto de excepciones por defecto, podrían no ser suficientes
para nuestro programa. En ese caso, deberíamos definir nuestra propia excepción.

Si queremos crear una excepción, solamente tenemos que crear una clase que herede de la
clase Exception. Es tan sencillo como el siguiente ejemplo.

# Creamos una excepción personalizada

class MiExcepcionPersonalizada(Exception):

pass

Y ya podríamos lanzarla con raise cuando quisiéramos.

raise MiExcepcionPersonalizada()

También se pueden pasar parámetros de entrada al lanzarla. Esto es muy útil para dar información
adicional a la excepción. En el siguiente ejemplo se pasan dos parámetros. Para ello tenemos que
modificar la función __init__() de la siguiente manera.
# Creamos nuestra propia excepción heredando

# de la clase Exception

class MiExcepcionPersonalizada(Exception):

def __init__(self, parametro1, parametro2):

self.parametro1 = parametro1

self.parametro2 = parametro2

Y una vez hemos creado nuestra excepción, podemos lanzarla con raise como ya hemos visto.
También es posible acceder a los parámetros pasados como argumentos al lanzar la excepción.

# Lanzamos con raise la excepción que hemos creado

try:

raise MiExcepcionPersonalizada("ValorPar1", "ValorPar2")

except MiExcepcionPersonalizada as ex:

p1, p2 = ex.args

print(type(ex))

print("parametro1 =", p1)

print("parametro2 =", p2)

#<class '__main__.MiExcepcionPersonalizada'>

#parametro1 = ValorPar1

#parametro2 = ValorPar2

## Ejemplos Hay un truco muy interesante que nos permite pasar a la excepción un argumento en
forma de diccionario como se muestra a continuación.

# Se define una excepción

class MiExcepcion(Exception):

pass

# Se lanza

try:

raise MiExcepcion({"mensaje":"Mi Mensaje", "informacion":"Mi Informacion"})


# Se captura

except MiExcepcion as e:

detalles = e.args[0]

print(detalles)

print(detalles["mensaje"])

print(detalles["informacion"])

#{'mensaje': 'Este es el mensaje', 'informacion': 'Esto es la informacion'}

# Mi Mensaje

# Mi Informacion

Como se puede ver, los parámetros son accesibles con [] ya que se trata de un diccionario.

Una forma un poco más larga de hacer lo mismo sería se la siguiente manera. En este caso los
parámetros que se pasan no son accesibles como si fueran un diccionario sino como se de un
objeto normal se tratase con .mensaje y .informacion

class MiExcepcion(Exception):

def __init__(self, mensaje, informacion):

self.mensaje = mensaje

self.informacion = informacion

try:

raise MiExcepcion("Mi Mensaje", "Mi Informacion")

except MiExcepcion as e:

print(e.mensaje)

print(e.informacion)

Nótese que para los ejemplos hemos usado mensaje en informacion, pero estas variables pueden
ser las que se te ocurran, y por supuesto tampoco tienen porque ser dos, podrían ser mas.

Gestores de contexto
Tal vez nunca hayas oído hablar de los gestores de contexto o context managers, pero si has
trabajado con ficheros ya los has usado sin darte cuenta. Si alguna vez has visto la cláusula with,
todo lo que pasa por debajo hace uso de los gestores de contexto.

Realmente no ofrecen ninguna funcionalidad nueva, pero permiten ahorrar código eliminando todo
lo que sea repetitivo o boilerplate. En pocas palabras, permiten ejecutar dos tareas de manera
automática, la primera al entrar al with y la segunda al salir del mismo.

El ejemplo más típico es el siguiente. Abrimos un fichero, escribimos contenido en él, y lo


cerramos.

# Haciendo uso de los context managers

with open('fichero.txt', 'w') as fichero:

fichero.write('Hola!')

¿Cómo que lo cerramos? Pues sí, aunque no se especifique expresamente, por debajo Python
ejecutará la función close() cuando se salga del bloque with. Es importante notar también que la
variable fichero no será accesible fuera del with, únciamente dentro.

El siguiente código es totalmente equivalente al anterior, pero sin hacer uso de los context
managers, simplemente de las excepciones.

# Sin usar los context managers

fichero = open('fichero.txt', 'w')

try:

fichero.write('Hola!')

finally:

fichero.close()

Como puedes ver, nos podemos ahorrar algunas líneas de código usando los gestores de contexto.
Su uso también nos permite dotar a nuestro código de mayor expresivadad, una de las grandes
ventajas de Python.

Su uso se extiende también a otras clases como Lock y es también común verlos en bases de
datos. Siempre que tengamos unos recursos que son bloqueados para ser usados, y después
necesiten ser liberados pase lo que pase (aunque ocurra una excepción), los gestores de contexto
serán una buena idea.

Llegados a este punto ya sabemos usar los gestores de contexto que vienen con Python, pero ¿y si
quisiéramos definir uno nosotros? A continuación lo vemos.

Implementación con clases

Si quieres definir tu propio gestor de contexto, existen dos formas de hacerlo:


• Con una clase: Implementando los métodos dunder __enter__ y __exit__ en tu clase.

• Con decoradores: Usando el decorador @contextmanager.

Veamos la primera forma usando clases. Lo primero que tenemos que hacer es definir nuestra
clase, e implementar los siguientes métodos:

• __init__: Este método es llamado automáticmente al entrar al bloque with. Lo que devuelva
este método será asignado a la variable que especifiquemos a continuación del as. Es
común que esto sea el recurso que vamos a utilizar, un fichero por ejemplo.

• __exit__: Este método será llamado al salir del with y contiene las tareas de limpieza que
queremos ejecutar. Lo más importante es que este método se llama siempre, incluso
aunque ocurra una excepción. Sería por lo tanto equivalente al uso del bloque except.
Trabajando con ficheros, aquí se cerraría el archivo que ha sido abierto anteriormente.

Veamos un ejemplo. Implementamos los métodos descritos con un simple print() para ver lo que
pasa. Podemos ver como efectivamente son llamados.

class MiGestor:

def __enter__(self):

print("Entra en __enter__")

def __exit__(self, exc_type, exc_value, traceback):

print("Entra en __exit__")

with MiGestor() as f:

print("Hola")

# Entra en __enter__

# Hola

# Entra en __exit__

Como se puede ver, Python llama por debajo a ambos métodos, primero al __enter__ y después
al __exit__.

Vamos a complicarlo un poco más. Como hemos indicado, el método __exit__ es ejecutado aunque
ocurra una excepción. Vamos por lo tanto a forzar una y ver que pasa.

with MiGestor() as f:

raise Exception
# Entra en __enter__

# Entra en __exit__

Como era de esperar, el contenido del método __exit__ también es ejecutado. Tal vez te hayas fijado
en los atributos de entrada del método. Son usados para obtener más información sobre la
excepción que ocurrió y poder actuar en consecuencia. Son los siguientes:

• exc_type: Tipo de excepción que fue lanzada. En nuestro ejemplo sería <class 'Exception'>

• exc_value: Valor de la excepción en el caso de que fuera proporcionado.

• traceback: Objecto traceback con más información acerca de la excepción.

Una vez sabido esto, ya estamos en condiciones de implementar nuestro propio gestor de contexto
con un ejemplo un poco más realista. Vamos a crear nuestro propia clase que envuelva a un fichero
con un gestor de contexto. Esta clase abrirá y cerrará un fichero haciendo uso de los gestores de
contexto.

class MiClaseFichero:

def __init__(self, nombre_fichero):

self.nombre_fichero = nombre_fichero

def __enter__(self):

self.fichero = open(self.nombre_fichero, 'w')

return self.fichero

def __exit__(self, exc_type, exc_val, exc_tb):

if self.fichero:

self.fichero.close()

Vayamos parte por parte:

• En el __init__ guardamos el nombre del fichero que queremos crear, nada nuevo.

• En el __enter__ creamos un fichero, lo almacenamos en nuestra clase, y devolvemos la


referencia que será usada dentro del with.

• Por último en el __exit__ cerramos el fichero si estaba abierto.

Una vez definida la clase, ya estamos en condiciones de usarla como hemos visto anteriormente.

with MiClaseFichero("fichero.txt") as fichero:

fichero.write("Hola!")
Por supuesto se trata de un ejemplo didáctico, si quieres leer un fichero simplemente usa las
funciones que Python proporciona por defecto.

Implementación con decoradores

La programación orientada a objetos es muy útil, pero no conviene abusar de ella. Tal vez te
encuentres en una situación donde no sea realmente necesario crear una clase. Por suerte,
también podemos definir gestores de contexto con decoradores.

Para ello puedes usar la librería contextlib. Su uso es muy similar pero tal vez sea un poco más
complejo si no conoces los generadores y el uso del yield.

from contextlib import contextmanager

@contextmanager

def gestor_fichero(nombre_fichero):

try:

fichero = open(nombre_fichero, 'w')

yield fichero

finally:

fichero.close()

Como puedes ver, el contenido del try sería el equivalente al contenido del __enter__ y el finally al
del __exit__. Una vez tenemos definida nuestra función, podemos usarla de la misma forma que
hemos visto anteriormente.

with gestor_fichero("fichero.txt") as fichero:

fichero.write("Hola!")

Anidando diferentes with

Es posible también anidar diferentes with, es decir, realizar una nueva llamada al with sin haber
salido del bloque anterior.

Esto puede dar lugar a códigos de lo más creativos como el que mostramos a continuación. Se trata
de un generador de índices, como el que se podría encontrar en un libro. Cada vez que se crea un
nuevo bloque with, se añade un nuevo nivel y se van numerando de cero a n, lo que modifica la
función print.

class Indice:

def __init__(self):

self.level = -1
self.numeracion = [0]

def __enter__(self):

self.level += 1

self.numeracion.append(0)

return self

def __exit__(self, exc_type, exc_val, exc_tb):

#self.numeracion[self.level] = 0

self.numeracion.pop()

self.level -= 1

def print(self, text):

self.numeracion[self.level] += 1

numer = [str(i) for i in self.numeracion[:self.level+1]]

print(f"{' '*self.level}{'.'.join(numer)} {text}")

Usando la clase Indice, podemos generar el índice de un libro. La llamada a la función print del
índice tendrá una funcionalidad distinta dependiendo de en que bloque se encuentre su llamada.
Es decir, la función imprime algo diferente dependiendo del contexto en el que se esté,
entendiendo por contexto el número de bloques with que haya anidados.

with Indice() as indice:

indice.print('Apartado')

with indice:

indice.print('Apartado')

indice.print('Apartado')

indice.print('Apartado')

indice.print('Apartado')

with indice:

indice.print('Apartado')

indice.print('Apartado')
with indice:

indice.print('Apartado')

indice.print('Apartado')

indice.print('Apartado')

indice.print('Apartado')

# 1 Apartado

# 1.1 Apartado

# 1.2 Apartado

# 1.3 Apartado

# 1.4 Apartado

# 1.4.1 Apartado

# 1.4.2 Apartado

# 1.4.2.1 Apartado

# 1.4.2.2 Apartado

# 2 Apartado

# 3 Apartado

Ficheros

• 📙📙 Leer archivos

• 📙📙 Escribir archivos

Leer archivos en Python

Al igual que en otros lenguajes de programación, en Python es posible abrir ficheros y leer su
contenido. Los ficheros o archivos pueden ser de lo más variado, desde un simple texto a contenido
binario. Para simplificar nos centraremos en leer un fichero de texto. Si quieres aprender como
escribir en un fichero te lo explicamos en este otro post.

Imagínate entonces que tienes un fichero de texto con unos datos, como podría ser un .txt o
un .csv, y quieres leer su contenido para realizar algún tipo de procesado sobre el mismo. Nuestro
fichero podría ser el siguiente.

# contenido del fichero ejemplo.txt

Contenido de la primera línea


Contenido de la segunda línea

Contenido de la tercera línea

Contenido de la cuarta línea

Podemos abrir el fichero con la función open() pasando como argumento el nombre del fichero que
queremos abrir.

fichero = open('ejemplo.txt')

Método read()

Con open() tendremos ya en fichero el contenido del documento listo para usar, y podemos
imprimir su contenido con read(). El siguiente código imprime todo el fichero.

print(fichero.read())

#Contenido de la primera línea

#Contenido de la segunda línea

#Contenido de la tercera línea

#Contenido de la cuarta línea

Método readline()

Es posible también leer un número de líneas determinado y no todo el fichero de golpe. Para ello
hacemos uso de la función readline(). Cada vez que se llama a la función, se lee una línea.

fichero = open('ejemplo.txt')

print(fichero.readline())

print(fichero.readline())

# Contenido de la primera línea

# Contenido de la segunda línea

Es muy importante saber que una vez hayas leído todas las línea del archivo, la función ya no
devolverá nada, porque se habrá llegado al final. Si quieres que readline() funcione otra vez, podrías
por ejemplo volver a leer el fichero con open().

Otra forma de usar readline() es pasando como argumento un número. Este número leerá
un determinado número de caracteres. El siguiente código lee todo el fichero carácter por carácter.

fichero = open('ejemplo.txt')

caracter = fichero.readline(1)

while caracter != "":

#print(caracter)
caracter = fichero.readline(1)

## Método readlines()

Existe otro método llamado readlines(), que devuelve una lista donde cada elemento es una línea
del fichero.

fichero = open('ejemplo.txt')

lineas = fichero.readlines()

print(lineas)

#['Contenido de la primera línea\n', 'Contenido de la segunda línea\n',

#'Contenido de la tercera línea\n', 'Contenido de la cuarta línea']

De manera muy sencilla podemos iterar las líneas e imprimirlas por pantalla.

fichero = open('ejemplo.txt')

lineas = fichero.readlines()

for linea in lineas:

print(linea)

#Contenido de la primera línea

#Contenido de la segunda línea

#Contenido de la tercera línea

#Contenido de la cuarta línea

Argumentos de open()

Hasta ahora hemos visto la función open() con tan sólo un argumento de entrada, el nombre del
fichero. Lo cierto es que existe un segundo argumento que es importante especificar. Se trata
del modo de apertura del fichero. En la documentación oficial se explica en detalle.

• ‘r’: Por defecto, para leer el fichero.

• ‘w’: Para escribir en el fichero.

• ‘x’: Para la creación, fallando si ya existe.

• ‘a’: Para añadir contenido a un fichero existente.

• ‘b’: Para abrir en modo binario.

Por lo tanto lo estrictamete correcto si queremos leer el fichero sería hacer lo siguiente. Aunque el
modo r sea por defecto, es una buena práctica indicarlo para darle a entender a otras personas que
lean nuestro código que no queremos modificarlo, tan solo leerlo.
fichero = open('ejemplo.txt', 'r')

## Cerrando el fichero

Otra cosa que debemos hacer cuando trabajamos con ficheros en Python, es cerrarlos una vez que
ya hemos acabado con ellos. Aunque es verdad que el fichero normalmente acabará siendo
cerrado automáticamente, es importante especificarlo para evitar tener comportamientos
inesperados.

Por lo tanto si queremos cerrar un fichero sólo tenemos que usar la función close() sobre el mismo.
Por lo tanto tenemos tres pasos:

• Abrir el fichero que queramos. En modo texto usaremos ‘r’.

• Usar el fichero para recopilar o procesar los datos que necesitábamos.

• Cuando hayamos acabado, cerramos el fichero.

fichero = open('ejemplo.txt', 'r')

# Usar la variable fichero

# Cerrar el fichero

fichero.close()

Existen otras formas de hacerlo, como con el uso de excepciones que veremos en otros posts. Un
ejemplo sería el siguiente. No pasa nada si aún no entiendes el uso del try y finally, por ahora
quédate con que la sección finally se ejecuta siempre sin importar si hay un error o no. De esta
manera el close() siempre será ejecutado.

fichero = open('ejemplo.txt')

try:

# Usar el fichero

pass

finally:

# Esta sección es siempre ejecutada

fichero.close()

Y por si no fuera poco, existe otra forma de cerrar el fichero automáticamente. Si hacemos uso
se with(), el fichero se cerrará automáticamente una vez se salga de ese bloque de código.

with open('ejemplo.txt') as fichero:

# Usar el fichero. Se cerrará automáticamente

pass
Ejemplos

Como ya hemos visto readline() lee línea por línea el fichero. También hacemos uso de un bucle
while para leer líneas mientras que no se haya llegado al final. Es por eso por lo que
comparamos linea != '', ya que se devuelve un string vació cuando se ha llegado al final.

with open('ejemplo.txt', 'r') as fichero:

linea = fichero.readline()

while linea != '':

print(linea, end='')

linea = fichero.readline()

#Contenido de la primera línea

#Contenido de la segunda línea

#Contenido de la tercera línea

#Contenido de la cuarta línea

Nos podemos ahorrar alguna línea de código si hacemos lo siguiente, ya que readlines() nos
devuelve directamente una lista que podemos iterar con las líneas.

with open('ejemplo.txt', 'r') as fichero:

for linea in fichero.readlines():

print(linea, end='')

#Contenido de la primera línea

#Contenido de la segunda línea

#Contenido de la tercera línea

#Contenido de la cuarta línea

Pero puede ser simplificado aún más de la siguiente manera. Nótese que usamos el end='' para
decirle a Python que no imprima el salto de línea \n al final del print.

with open('ejemplo.txt', 'r') as fichero:

for linea in fichero:

print(linea, end='')

#Contenido de la primera línea

#Contenido de la segunda línea


#Contenido de la tercera línea

#Contenido de la cuarta línea

Escribir archivos en Python

A continuación te explicamos como escribir datos en un fichero usando Python. Imagínate que
tienes unos datos que te gustaría guardar en un fichero para su posterior análisis. Te explicamos
como guardarlos en un fichero, por ejemplo, .txt. Si también quieres aprender como leer un fichero
en Python te lo explicamos en este otro post.

Lo primero que debemos de hacer es crear un objeto para el fichero, con el nombre que queramos.
Al igual que vimos en el post de leer ficheros, además del nombre se puede pasar un segundo
parámetro que indica el modo en el que se tratará el fichero. Los más relevantes en este caso son
los siguientes. Para más información consulta la documentación oficial.

• ‘w’: Borra el fichero si ya existiese y crea uno nuevo con el nombre indicado.

• ‘a’: Añadirá el contenido al final del fichero si ya existiese (append end Inglés)

• ‘x’: Si ya existe el fichero se devuelve un error.

Por lo tanto con la siguiente línea estamos creando un fichero con el nombre datos_guardados.txt.

# Abre un nuevo fichero

fichero = open("datos_guardados.txt", 'w')

Si por lo contrario queremos añadir el contenido al ya existente en un fichero de antes, podemos


hacerlo en el modo append como hemos indicado.

# Abre un nuevo y añade el contenido al final

fichero = open("datos_guardados.txt", 'a')

Método write()

Ya hemos visto como crear el fichero. Veamos ahora como podemos añadir contenido. Empecemos
escribiendo un texto.

fichero = open("datos_guardados.txt", 'w')

fichero.write("Contenido a escribir")

fichero.close()

Por lo tanto si ahora abrimos el fichero datos_guardados.txt, veremos como efectivamente


contiene una línea con Contenido a escribir. ¿A que es fácil?

Es muy importante el uso de close() ya que si dejamos el fichero abierto, podríamos llegar a tener
un comportamiento inesperado que queremos evitar. Por lo tanto, siempre que se abre un
fichero es necesario cerrarlo cuando hayamos acabado.
Compliquemos un poco más las cosas. Ahora vamos a guardar una lista de elementos en el fichero,
donde cada elemento de la lista se almacenará en una línea distinta.

# Abrimos el fichero

fichero = open("datos_guardados.txt", 'w')

# Tenemos unos datos que queremos guardar

lista = ["Manzana", "Pera", "Plátano"]

# Guardamos la lista en el fichero

for linea in lista:

fichero.write(linea + "\n")

# Cerramos el fichero

fichero.close()

Si te fijas, estamos almacenando la línea mas \n. Es importante añadir el salto de línea porque por
defecto no se añade, y si queremos que cada elemento de la lista se almacena en una línea
distinta, será necesario su uso.

Método writelines()

También podemos usar el método writelines() y pasarle una lista. Dicho método se encargará de
guardar todos los elementos de la lista en el fichero.

fichero = open("datos_guardados.txt", 'w')

lista = ["Manzana", "Pera", "Plátano"]

fichero.writelines(lista)

fichero.close()

# Se guarda

# ManzanaPeraPlátano

Tal vez te hayas dado cuenta de que en realidad lo que se guarda es ManzanaPeraPlátano, todo
junto. Si queremos que cada elemento se almacene en una línea distinta, deberíamos añadir el
salto de línea en cada elemento de la lista como se muestra a continuación.
fichero = open("datos_guardados.txt", 'w')

lista = ["Manzana\n", "Pera\n", "Plátano\n"]

fichero.writelines(lista)

fichero.close()

# Se guarda

# Manzana

# Pera

# Plátano

Uso del with

Podemos ahorrar una línea de código si hacemos uso de lo siguiente. En este caso nos podemos
ahorrar la llamada al close() ya que se realiza automáticamente. El código anterior se podría
reescribir de la siguiente manera.

lista = ["Manzana\n", "Pera\n", "Plátano\n"]

with open("datos_guardados.txt", 'w') as fichero:

fichero.writelines(lista)

Ejemplos escribir ficheros en Python

El uso de ‘x’ hace que si el fichero ya existe se devuelve un error. En el siguiente código creamos un
fichero e inmediatamente después intentamos crear un fichero con el mismo nombre con la opción
‘x’. Por lo tanto se devolverá un error.

f = open("mi_fichero.txt", "w")

# f = open("mi_fichero.txt", "x")

# Error! Ya existe

En este otro ejemplo vamos a usar un fichero para establecer una comunicación entre dos
funciones. A efectos prácticos puede no resultar muy útil, pero es un buen ejemplo para mostrar la
lectura y escritura de ficheros.

Tenemos por lo tanto una función escribe_fichero() que recibe un mensaje y lo escribe en un fichero
determinado. Y por otro lado tenemos una función lee_fichero() que devuelve el mensaje que está
escrito en el fichero.
Date cuenta lo interesante del ejemplo, ya que podríamos tener estos dos códigos ejecutándose en
diferentes maquinas o procesos, y podrían comunicarse a través del fichero. Por un lado se escribe
y por el otro se lee.

# Escribe un mensaje en un fichero

def escribe_fichero(mensaje):

with open('fichero_comunicacion.txt', 'w') as fichero:

fichero.write(mensaje)

# Leer el mensaje del fichero

def lee_fichero():

mensaje = ""

with open('fichero_comunicacion.txt', 'r') as fichero:

mensaje = fichero.read()

# Borra el contenido del fichero para dejarlo vacío

f = open('fichero_comunicacion.txt', 'w')

f.close()

return mensaje

escribe_fichero("Esto es un mensaje")

print(lee_fichero())

Test y documentación en Python

• 📙📙 Python PEP8

• 📙📙 Nombrando Variables

• 📙📙 Argparse en Python

• 📙📙 Errores Comunes

• 📙📙 Código Pythonico

• 📙📙 Testing con assert y unittest


Python PEP8: Escribiendo Código Fácil de Leer

Introducción

La PEP8 es una guía que indica las convenciones estilísticas a seguir para escribir código Python.
Se trata de un conjunto de recomendaciones cuyo objetivo es ayudar a escribir código más legible y
abarca desde cómo nombrar variables, al número máximo de caracteres que una línea debe tener.

De acuerdo con Guido van Rossum, el código es leído más veces que escrito, por lo que resulta
importante escribir código que no sólo funcione correctamente, sino que además pueda ser leído
con facilidad. Esto es precisamente lo que veremos en este artículo.

Code is read much more often than it is written, Guido van Rossum

Dos mismos códigos pueden realizar lo mismo funcionalmente, pero si no se siguen unas
directrices estilísticas, se puede acabar teniendo un código muy difícil de leer. Los problemas más
frecuentes suelen ser:

• Líneas demasiado largas.

• Nombres de variables poco explicativos.

• Código mal comentado.

• Uso incorrecto de espacios y líneas en blanco.

• Código mal identado.

Aunque es cierto que ciertas directrices pueden resultar arbitrarias, Python define en la PEP8 las
normas estilísticas a seguir para cualquier código parte de la librería estándar, por lo que queda al
criterio de cada uno usar estas recomendaciones o no. Sin embargo, prácticamente cualquier
código o librería usado por gran cantidad de personas, emplea estas recomendaciones, al haber un
amplio consenso en la comunidad.

Formatear Código Python PEP8: Linters y Autoformatters

A veces puede resultar complicado acordarnos de todas y cada una de las normas de la PEP8, por
lo que hay herramientas que nos ayudan a corregir automáticamente o indicarnos donde hay
problemas en nuestro código. Hay dos tipos de herramientas:

• Los linters como flake8 o pycodestyle.

• Y los autoformatters como black y autopep8.

Los autoformatters se limitan a indicarnos donde nuestro código no cumple con las normas, y en
ciertos casos realiza las correcciones automáticamente. Por ejemplo, podemos
instalar autopep8 y se puede instalar de la siguiente manera:

$ pip install autopep8

Y si lo usamos sobre un script.py intentará corregir los problemas.


$ autopep8 script.py -v -i

Veamos un ejemplo. Para alguien recién iniciado en Python, tal vez el siguiente código parezca
válido, sin embargo alguien que conozca la PEP8 podrá identificar varios problemas.

# script.py

def MiFuncionSuma(A, B, C, imprime = True):

resultado=A+B+C

if imprime != False:

print(resultado)

return resultado

a =4

variable_b = 5

var_c = 10

MiFuncionSuma(a, variable_b, var_c)

Usando el comando anterior, se nos informará de todas las reglas que nuestro código no cumple,
para que las podamos corregir. Es importante notar que existen reglas que pueden ser corregidas
automáticamente, y otras que no.

• Todo lo relativo al uso de espacios o líneas en blanco, puede ser corregido


automáticamente por autopep8.

• Sin embargo autopep8 nunca modificará el nombre de una variable, por lo que si
incumplimos alguna norma en lo relativo a nombrar variables deberemos corregir de forma
manual las ocurrencias.

El código anterior incumple las siguientes reglas:

• E251: Uso incorrecto de espacios en imprime = True, debería ser imprime=True.

• E225: Los operadores como el + deben usar espacios, A + B + C.

• E712: Usar if imprime en vez de if imprime != False.

• E305: Después de la declaración de una función debemos dejar dos espacios en blanco.

• E221: No debemos usar tantos espacios al usar el operador = creando variables.

• También tenemos otros problemas relacionados con cómo nombrar a funciones y variables.
Las funciones y variables deben ir en snake case. Lo veremos en detalle más adelante.
Teniendo en cuenta lo mencionado, podemos implementar las correcciones para tener un código
que cumple con la PEP8.

# script.py

def mi_funcion_suma(a, b, c, imprime=True):

resultado = a + b + a

if imprime:

print(resultado)

return resultado

a=4

variable_b = 5

var_c = 10

mi_funcion_suma(a, variable_b, var_c)

Visto ya un ejemplo concreto, a continuación veremos las normas más importantes introducidas
en la PEP8.

Organización del código

Líneas en Blanco

El uso de líneas en blanco mejora notablemente la legibilidad. Mucho código seguido puede ser
difícil de leer, pero un uso excesivo de líneas en blanco puede ser molesto. Python deja su uso a
nuestro criterio, siempre y cuando cumplamos lo siguiente:

• Rodear las funciones y clases con dos líneas en blanco. Cada vez que definamos una clase o
una función es necesario dejar dos líneas en blanco por arriba y dos por abajo.

• Dejar una línea en blanco entre los métodos de una clase. Los métodos de una clase
deberán tener una línea en blanco entre ellos.

• Usar líneas en blanco para agrupar pasos similares. Si tenemos un conjunto de código que
realiza una función concreta, es conveniente delimitarlo con una línea en blanco, de la
misma manera que un libro separa ideas en párrafos.

# 1 espacio entre métodos

# 2 espacios entre clases y funciones


class ClaseA:

def metodo_a(self):

pass

def metodo_b(self):

pass

class ClaseB:

def metodo_a(self):

pass

def metodo_b(self):

pass

def funcion():

pass

También resulta conveniente separar con una línea diferentes funcionalidades. La siguiente
función calcula la media y la mediana, por lo que las separamos con una línea en blanco.

def calcula_media_mediana(valores):

# Calculamos la media

suma_valores = 0

for valor in valores:

suma_valores += valor

media = suma_valores / len(valores)

# Calculamos la mediana

valores_ordenados = sorted(valores)
indice = len(valores) // 2

if len(valores) % 2:

mediana = valores_ordenados[indice]

else:

mediana = (valores_ordenados[indice]

+ valores_ordenados[indice + 1]) / 2

return media, mediana

Espacios en Blanco

El uso de espacios en blanco puede resultar clave para mejorar la legibilidad de nuestro código, y
es por lo que la PEP8 nos dice dónde debemos usar espacios y dónde no. Se trata de buscar un
punto de equilibrio entre un código demasiado disperso y con gran cantidad de espacios, y un
código demasiado junto donde no se identifican sus partes.

Se nos recomienda usar espacio con operadores de asignación.

# Correcto

x=5

# Incorrecto

x=5

Y también con operadores relacionales.

# Correcto

if x == 5:

pass

# Incorrecto

if x==5:

pass

Pero cuando tengamos funciones con argumentos por defecto, no debemos dejar espacios.

# Correcto

def mi_funcion(parameto_por_defecto=5):
print(parameto_por_defecto)

# Incorrecto

def mi_funcion(parameto_por_defecto = 5):

print(parameto_por_defecto)

Por otro lado se recomienda no dejar espacios dentro del paréntesis.

def duplica(a):

return a * 2

# Correcto

duplica(2)

# Incorrecto

duplica( 2 )

Y tampoco entre corchetes.

# Correcto

lista = [1, 2, 3]

# Incorrecto

my_list = [ 1, 2, 3, ]

El uso de los espacios resulta muy útil cuando se combinan varios operadores utilizando diferentes
variables, utilizando los espacios para agrupar por orden de mayor prioridad. Es por ello por lo que
no dejamos espacios en x**2 ni (x-y) dado que la potencia y el uso de paréntesis son los operadores
con mayor prioridad.

# Correcto

y = x**2 + 1

z = (x-y) * (x+y)

# Incorrecto

y = x ** 2 + 5
z = (x - y) * (x + y)

Siguiendo la misma filosofía de agrupar por orden de ejecución, tenemos los siguiente ejemplos,
siendo el primero el preferido por algunos linters.

# Correcto

if x > 0 and x % 2 == 0:

print('...')

# Correcto

if x>0 and x%2==0:

print('...')

# Incorrecto

if x% 2 == 0:

print('...')

No usar espacio antes de , en llamadas a funciones o métodos.

# Correcto

print(x, y)

# Incorrecto

print(x , y)

Cuando usemos listas no usar espacios antes del índice o entre el índice y los [].

# Correcto

lista[0]

# Incorrecto

lista [1]

# Incorrecto

lista [ 1 ]
Tampoco usando diccionarios.

# Correcto

diccionario['key'] = lista[indice]

# Incorrecto

diccionario ['key'] = lista [indice]

Por último y aunque pueda parecer raro para la gente que venga de otros lenguajes de
programación, no se recomienda alinear las variables como se muestra a continuación.

# Correcto

var_a = 0

variable_b = 10

otra_variable_c = 3

# Incorrecto

var_a =0

variable_b = 10

otra_variable_c = 3

Identación del código

Como ya hemos visto en otros artículos, Python no usa {} para designar bloques de código como
otros lenguajes de programación, sino que usa bloques identados para indicar que un determinado
bloque de código pertenece a por ejemplo un if.

if x > 5:

pass

Un bloque identado se representa usando cuatro espacios y aunque el uso del tabulador pueda
parecer lo mismo, Python 3 no recomienda su uso. Como regla de oro:

• Usa siempre cuatro espacios.

• Usa tabuladores si trabajas sobre código ajeno que ya use tabuladores.

• Bajo ningún concepto mezcles uso de espacios y tabuladores.

Por otro lado, también se puede identar el código para evitar tener líneas muy largas, que resultan
difíciles de leer. Es importante recordar que la PEP8 limita el tamaño de línea a 79 caracteres.
# Correcto

def mi_funcion(primer_parametro, segundo_parametro,

tercer_parametro, cuarto_parametro,

quinto_parametro):

print("Python")

# Incorrecto

def mi_funcion(primer_parametro, segundo_parametro, tercer_parametro, cuarto_parametro,


quinto_parametro):

print("Python")

Lo siguiente sería incorrecto ya que no se diferencian los argumentos de entrada del bloque de
código a ejecutar por la función.

# Incorrecto

def mi_funcion(primer_parametro, segundo_parametro,

tercer_parametro, cuarto_parametro,

quinto_parametro):

print("Python")

Análogamente se puede romper un if en diferentes líneas, útil cuando se usan gran cantidad de
condiciones que no entran una una línea.

# Correcto

if (condicion_a and

condicion_b):

print("Python")

Tamaño de linea

Se recomienda limitar el tamaño de cada línea a 79 caracteres, para evitar tener que hacer scroll a
la derecha. Este límite también permite tener abiertos múltiples ficheros en la misma pantalla, uno
al lado de otro. Por otro lado se limita el uso de docstrings y comentarios a 72 caracteres.

En los casos que tengamos una línea que no sea posible romper, podemos usar \ para continuar
con la línea en una nueva. Esto es algo que a veces puede darse en los context managers.

# Correcto

with open('/esta/ruta/es/muy/pero/que/muy/larga/y/no/entra/en/una/sola/linea/') as fichero_1, \


open('/esta/ruta/es/muy/pero/que/muy/larga/y/no/entra/en/una/sola/linea/', 'w') as fichero_2:

fichero_2.write(fichero_1.read())

Operaciones largas

Si queremos realizar una operación muy larga que no entra en una línea, tendremos que dividirla en
múltiples. Lo recomendado es usar el operador al principio de cada línea, ya que resulta mas fácil
de leer.

# Recomendado

income = (variable_a

+ variable_b

+ (variable_c - variable_d)

- variable_e

- variable_f)

La siguiente opción no es recomendada pero la PEP8 tampoco la prohíbe.

# No recomendado

income = (variable_a +

variable_b +

(variable_c - variable_d) -

variable_e -

variable_f)

Codificación de ficheros

Los ficheros se codifican por defecto en ASCII para Python 2 y UTF-8 para Python 3, por lo que será
necesario definir la codificación que usemos cuando queramos usar otro tipo.

Esto resulta muy importante, ya que si queremos almacenar una cadena que contiene caracteres
no UTF-8 como ó y ñ, deberemos especificar el tipo de encoding de acuerdo a la PEP263. El
siguiente código puede dar problemas.

print("La acentuación del Español")

# SyntaxError: Non-UTF-8 code starting with '\xf3' in file script.py on line 1, but no encoding
declared; see http://python.org/dev/peps/pep-0263/ for details

Sin embargo, con un pequeño cambio, podemos cambiar la forma en la que se codifica el texto.

# -*- coding: latin-1 -*-

print("La acentuación del Español")


# La acentuación del Español

Por otro lado, si tienes intención de desarrollar código para la librería estándar de Python, o
contribuir en un proyecto con alcance global, debes saber lo siguiente:

• Todos los identificadores (como variables) deben usar ASCII.

• También deberán usar Inglés en la medida de lo posible, salvo abreviaciones.

• Las únicas excepciones son los test para código no ASCII y los nombres de autores.

Convenciones al Nombrar Elementos: CamelCase y snake_case

A la hora de escribir código, todo tiene nombres: variables, clases, funciones, paquetes, módulos,
etc. Es por lo tanto muy importante seguir unas directrices determinadas para que nuestro código
sea lo más legible posible. No se nombra igual a una clase que a una función, y tampoco suele ser
recomendable usar nombres como a o x ya que aporta poca información. A continuación lo vemos
en detalle.

Eligiendo Nombres

Antes de nada debemos debemos pensar el nombre que le vamos dar a nuestra variable clase o
función. Es importante tener en cuenta lo siguiente:

• Evitar usar palabras reservadas. Si es necesario usar una palabra reservada como class,
usar class_ como alternativa.

• Evitar usar l O y I, ya que pueden ser confundidas.

• Usar _variable para especificar uso interno. Por ejemplo from m import * no importaría lo que
empieza con _.

• Se puede usar __variable para invocar el name mangling y hacer privadas determinadas
variables o métodos.

• Para métodos mágicos usar siempre __init__, pero no son nombres que debemos crear sino
reutilizar los que Python nos ofrece.

Estilos: Camel Case y snake_case

Supongamos que ya sabemos como vamos a nombrar a nuestra clase, función o variable.
Pongamos que queremos llamar a nuestra función “mi función de prueba”. Dado que no podemos
utilizar espacios para nombrar variables, hay diferentes alternativas:

• mi_funcion_de_prueba

• MiFuncionDePrueba

• MIFUNCIONDEPRUEBA

• MI_FUNCION_DE_PRUEBA

• mifunciondeprueba
Algunas de estas alternativas son conocidas como Camel Case o snake_case en el mundo de la
programación. Pues bien, Python define cómo nombrar a cada tipo de la siguiente manera:

• Funciones: Letras en minúscula separadas por barra baja: funcion, mi_funcion_de_prueba.

• Variables: Al igual que las funciones: variable, mi_variable.

• Clases: Uso de CamelCase, usando mayúscula y sin barra baja: MiClase, ClaseDePrueba.

• Métodos: Al igual que las funciones, usar snake case: metodo, mi_metodo.

• Constantes: Nombrarlas usando mayúsculas y separadas por barra


bajas: UNA_CONSTANTE, OTRA_CONSTANTE.

• Módulos: Igual que las funciones: modulo.py, mi_modulo.py.

• Paquetes: En minúsculas pero sin separar por barra bajas: packete, mipaquete

En el siguiente fragmento podemos ver su uso.

# mi_script.py

CONSTANTE_GLOBAL = 10

class MiClase():

def mi_primer_metodo(self, variable_a, variable_b):

return (variable_a + variable_b) / CONSTANTE_GLOBAL

mi_objeto = MiClase()

print(mi_objeto.mi_primer_metodo(5, 5))

Importando Paquetes: Orden y Organización

Los import deben separarse en diferentes líneas.

# Correcto

import os

import sys

# Incorrecto

import os, sys

Sin embargo cuando se importen varios elementos de una misma librería, si sería correcto
importarlos en la misma línea.
# Correcto

from subprocess import Popen, PIPE

Con respecto a su ubicación, deberán seguir la siguiente:

• Deben ir al principio del fichero.

• Después de comentarios del módulo y docstrings.

• Antes de los global y las constantes.

Con respecto a su organización, debiendo haber una línea de separación entre cada grupo:

• Primero las librerías estándar.

• Segundo las librerías externas.

• Tercero las librerías locales.

Con respecto a su tipo:

• Se recomienda usar imports absolutos.

• Aunque también se permiten los relativos.

Por último, deben evitarse el from <módulo> import *. El uso de * importa todo lo presente en
el <módulo>, por lo que no queda claro que se está usando y que no.

# Incorrecto

from collections import *

Si por ejemplo usamos únicamente deque y defaultdict, indicarlo.

# Correcto

from collections import deque, defaultdict

Comas al Final de Línea

El uso de comas al final de la línea suele ser opcional, salvo cuando se quiera crear tuplas de un
sólo elemento como se muestra a continuación.

# Correcto

tupla = (1,)

print(tupla[0])

# Salida: 1

Sin embargo aunque su uso sea opcional en el resto de casos, en ciertas ocasiones puede estar
justificado si por ejemplo tenemos una lista de elementos que puede cambiar con el tiempo. En
este caso el uso de , al final puede ser de ayuda al sistema de control de versiones que utilicemos
(como Git).

# Correcto

FICHEROS = [

'fichero1.txt',

'fichero2.txt',

# Incorrecto

FICHEROS = ['fichero1.txt', 'fichero2.txt',]

Comentarios

Los comentarios son muy importantes para realizar anotaciones a futuros lectores de nuestro
código, y aunque resulta difícil definir cómo se se debe comentar el código, hay ciertas directrices
que debemos seguir:

• Cualquier comentario que contradiga el código es peor que ningún comentario. Por ello es
muy importante que si actualizamos el código, no olvidarnos de actualizar los comentarios
para evitar crear inconsistencias.

• Los comentarios deben ser frases completas, con la primera letra en mayúsculas.

• Si el comentario es corto, no hace falta usar el punto y final.

• Si el código es comentado en Inglés, usar Strunk/White.

• Aunque cada uno es libre de escribir sus comentarios en el idioma que considere oportuno,
se recomienda hacerlo en Inglés.

• Evitar comentarios poco descriptivos que no aporten nada más allá de lo que ya se ve a
simple vista.

• En lo relativo a los comentarios docstrings, usar la PEP257 como referencia.

A modo de ejemplo, como hemos explicado es conveniente evitar comentarios redundantes.

# Incorrecto

x=x+1 # Suma 1 a la variable x

# Correcto

x=x+1 # Compensa el offset producido por la medida


Nombrando Variables en Python

Estoy seguro de que alguna vez has visto métodos, atributos o funciones que tienen algún tipo de
guión o barra baja _ en su nombre. La verdad que al principio puede resultar un poco confuso, ya
algunos pueden tenerlo al principio otros pueden tener incluso doble barra baja y otros pueden
tenerlo al principio y al final. Todo un lío. Veamos las posibilidades:

• Barra baja al principio _nombre

• Barra baja al final nombre_

• Doble barra baja al principio __nombre

• Doble barra baja al principio y final __nombre__

• Barra baja sin contenido _

Es importante notar que el uso de _ puede significar dos cosas. Por un lado puede significar
una mera convención que no alterará el comportamiento de nuestro código. Por otro lado, en
algunos casos su uso si modifica el comportamiento y su uso es relevante. Lo vemos a
continuación.

Al inicio: _nombre

El uso de _ antes del nombre de una variable, como podría ser _mi_variable no modifica el
comportamiento del código. Su uso es una mera convención que ha sido acordada por los usuarios
de Python, y que indica que esa variable no debería ser accedida desde fuera de la clase, pero
puede serlo.

class Clase:

def __init__(self):

self._variable = 10

mi_clase = Clase()

# No se debería hacer, pero se puede

mi_clase._variable # 10

Su uso si que puede modificar el comportamiento del código usado con funciones. Una función es
definida con _ no es importada por defecto si usamos from x import *.

Al Final: nombre_

Para entender el uso de la barra baja al final de una variable o función, es importante saber que
Python tiene un determinado conjunto de palabras reservadas o keywords. Estas palabras no
pueden ser usadas, porque de serlo Python se confundiría y no sabría como interpretarlas. Si por el
motivo que sea, queremos llamar a una variable con el mismo nombre que una palabra reservada,
podemos hacer algo así como class_. El siguiente código muestra que pasa si intentamos usar una
palabra reservada para llamar a una variable.

#class = 5 # Error! class es una palabra reservada

Podemos usar _ para solucionarlo. Nótese que a diferencia de otras formas de usar _ en este
caso no modifica comportamiento, por lo que su uso es arbitrario.

class_ = 5

def_ = 10

Doble al Principio: __nombre

A diferencia del uso de una sola barra baja, el uso de la doble __ en un atributo o método hace que
Python lo oculte al exterior. Existe un concepto muy interesante y particular de Python
llamado name mangling, del que te recomendamos leer más.

Por lo tanto, en el siguiente ejemplo __nombre no será accesible desde el exterior de la clase. Por
supuesto si que podría accederse desde la propia clase.

class Clase:

def __init__(self):

self.__variable = 10

mi_clase = Clase()

#mi_clase.__variable # Error! No es accesible

Doble al Principio y Final: nombre

Por último, el uso de __ al principio y al final de un nombre tiene especial relevancia cuando
es aplicado a métodos. De hecho, a lo largo de este post ya has visto el uso de __init__ varias veces.
Se trata de una forma que usa Python para designar a los conocidos como métodos mágicos como
pueden ser también el __call__ o el __le__. Por norma general, es mejor no definir métodos propios
con estos nombres, y limitarse a utilizar los que Python ya ofrece.

class Clase:

def __init__(self):

print("Init")

Descartando Variables con Barra Baja

Su uso suele ser común cuando queremos descartar una variable que por ejemplo pueda devolver
una función. Veamos un ejemplo de una función que devuelve dos parámetros, la suma y la resta
de dos variables.
def sumayresta(a, b):

return (a+b), (a-b)

Tal vez queramos sólo el valor de la suma y no nos interese el valor de la resta. Para ello podríamos
hacer lo siguiente.

suma, _ = sumayresta(5, 5)

# 10

Es una forma de decir “no me interesa esta variable”, pero como no se puede dejar el hueco vacío,
se usa _ para rellenarlo.

Argparse en Python

Introducción al Command Line Interface

Los Command Line Interface o interfaces de línea de comandos (comúnmente abreviado como CLI)
son una herramienta que ofrecen gran cantidad de programas para interactuar con ellos. Si alguna
vez has usado el comando ls o mkdir de Linux, ya has usado un CLI.

El comando ls permite listar directorios y ficheros.

$ ls -ta

El comando mkdir permite la creación de directorios, de ahí su nombre de make dir.

$ mkdir -p foo

En ambos comandos podemos identificar tres partes:

• Por un lado tenemos el comando ls o mkdir, que determina el código o librería que se va a
usar.

• Después tenemos las opciones como -ta o -p, que modifican el comportamiento del
comando que precede. Por ejemplo, la opción -t ordena los ficheros por orden de
modificación.

• Por ultimo tenemos los argumentos que se le pasan al comando. Pueden no existir como en
el caso de ls, o tener uno como en el caso de mkdir, que indica el nombre del fichero a crear.

Los CLI son una herramienta perfecta para exponer tu código a que pueda ser usado por otras
personas, de manera sencilla y encapsulando el contenido que está en el interior. De ahí su nombre
de interfaz, que no es gráfico, sino de línea de comandos.

Command Line Interfaces en Python

Vamos a ver un ejemplo más relacionado con Python. Imagina que nos han encargado hacer un
software para resolver un problema determinado, y que ya tenemos todo el código listo y
funcionando. Por las características del proyecto, dicho software no tendrá un interfaz gráfico de
usuario, pero si que será necesario recibir diferentes parámetros para cambiar el comportamiento
del programa. Es decir, tenemos unas variables que serán recibidas desde fuera.

Supongamos que tenemos el siguiente código, donde tenemos dos parámetros a y b que se suman,
restan o multiplican en función del valor de operacion.

# calculadora.py

# Tipo de operacion: suma/resta/multiplicación

operacion = "suma"

# Parámetros de la operación

a=4

b=7

if operacion == "suma":

print(a+b)

elif operacion == "resta":

print(a-b)

elif operacion == "multiplicacion":

print(a*b)

Lo que queremos por tanto es que los parámetros a, b y operacion sean recibidos desde fuera de
nuestro programa, a través de la línea de comandos.

Imaginémonos que la persona que va a usar este código ni si quiera sabe Python, pero quiere usar
nuestro software como una calculadora. En este caso necesitaremos abstraer al usuario del
código, y darle una ventana al exterior desde la que pueda simplemente decir el valor de a, b y
la operacion, y obtener el resultado.

Nuestro objetivo es por tanto buscar una manera en la que un usuario pueda pasar por terminal los
valores de a, b y operacion para que sean usados por el código. Una forma sería la siguiente.

$ python calculadora.py 5 2 suma

Otra opción es usando nombres para los argumentos.

$ python calculadora.py --a=5 --b=2 --operacion=suma


Este será por tanto el problema que resolveremos a lo largo de este artículo, utilizando diferentes
maneras y explorando las opciones que Python nos ofrece.

Creando un CLI sin argparse

La primera forma que tenemos de que un script reciba argumentos desde el terminal es usando el
módulo sys.

# calculadora.py

import sys

print(sys.argv)

La siguiente llamada captura todo lo que se pasa después de python. Como puedes ver, el primer
elemento argv[0] nos devuelve el nombre del script. Por otro lado, se capturan todos y cada uno de
los argumentos que se pasan a la derecha del mismo.

$ python calculadora.py 5 2 suma

['calculadora.py', '5', '2', 'suma']

Con esta lista ya tendríamos todo lo que necesitamos para usar nuestra calculadora.py, pero existe
una manera mucho mejor de tratar los argumentos, y es usando la librería argparse. Veamos como
funciona.

Creando un CLI con argparse

La librería argparse fue introducida en Python en la PEP389 y permite crear CLI para todo tipo de
proyectos, ofreciendo un amplio abanico de posibilidades útiles hasta en los proyectos más
complejos.

Introducción a argparse

Vamos a comenzar con una primera aproximación de lo que sería el uso de argparse, creando un
interfaz para nuestra calculadora mostrada anteriormente:

• Importamos la librería.

• Creamos un ArgumentParser.

• Añadimos argumentos.

• Y parseamos los argumentos.

# script.py

import argparse

parser = argparse.ArgumentParser()

parser.add_argument("a")

parser.add_argument("b")
parser.add_argument("operacion")

args = parser.parse_args()

variables = vars(args)

print(variables)

Si llamamos al código anterior en el terminal, podemos ver como ya tenemos todo lo necesario
para nuestra calculadora.

$ python script.py 7 7 suma

{'a': '7', 'b': '7', 'operacion': 'suma'}

Con esta primera aproximación ya tendríamos todas las variables que necesitamos en
el diccionario variables, pero aunque pueda parecer que ya hemos resuelto nuestro problema,
tenemos los siguientes problemas:

• Estamos usando parámetros posicionales, lo que significa que estamos obligados a pasar
primero a, después b y por último c. Si pasamos los argumentos en otro orden, romperemos
el programa.

• No especificamos los tipos de las variables, por defecto todas son string cuando en
realidad a y b deberían ser integer. Esto es un problema ya que 7+7 con cadenas es 77.

• El parámetro operacion debería tomar sólo valores discretos, ya que únicamente queremos
suma/resta/multiplicación. Deberíamos forzar un error si se pasa una operación no
soportada.

• Por defecto, todos los parámetros son obligatorios, pero esto puede no ser siempre el caso.
Imagina que por defecto queremos sumar, en el caso de que no se especifique la operacion.

• Por último, no estamos ofreciendo ninguna documentación, por lo que la persona que tenga
nuestro software no sabrá como usarlo.

En vista a lo anterior, podemos realizar las siguientes modificaciones en el código para hacerlo más
correcto. Fijémonos en los cambios.

• El uso de -a y --numero_a permite indicar el nombre del parámetro que estamos pasando,
por lo que ya no será necesario hacerlo en un orden fijo. Se utiliza un guión para la
abreviación del argumento y dos para el nombre completo.

• Añadimos type a cada argumento.

• Añadimos choices a la operación, para que de un error si seleccionamos una no soportada.

• Ponemos la operación como parámetro opcional required=False y asignamos un valir por


defecto default en el caso de que no se proporcione.
• Añadimos documentación con description y help para que un usuario ajeno a nuestro código
sepa utilizarlo.

Poniéndolo todo junto en nuestro programa calculadora.py, tendríamos algo como lo siguiente.

# calculadora.py

import argparse

parser = argparse.ArgumentParser(description='Calculadora, suma/resta/multiplica a y b')

parser.add_argument('-a', '--numero_a', type=int, help='Parámetro a')

parser.add_argument('-b', '--numero_b', type=int, help='Parámetro b')

parser.add_argument('-o', '--operacion',

type=str,

choices=['suma', 'resta', 'multiplicacion'],

default='suma', required=False,

help='Operación a realizar con a y b')

args = parser.parse_args()

if args.operacion == 'suma':

print(args.numero_a + args.numero_b)

elif args.operacion == 'resta':

print(args.numero_a - args.numero_b)

elif args.operacion == 'multiplicacion':

print(args.numero_a * args.numero_b)

Como vemos a continuación, hay diferentes formas de realizar la llamada a nuestro script y son
todas equivalentes. La forma más sencilla es usando las abreviaciones como se muestra a
continuación.

$ python calculadora.py -a 1 -b 4 -o suma

Pero es también posible usar el nombre completo.

$ python calculadora.py --numero_a 7 --numero_b 3 --operacion multiplicacion


21

O también podemos usar el signo igual = para realizar la asignación.

$ python calculadora.py -a=7 -b=3 --operacion=multiplicacion

21

Podemos alterar el orden de los parámetros ya que no son posicionales.

$ python calculadora.py -b=3 --operacion=multiplicacion -a=7

21

Una vez vistas como se realizarían las llamadas, veamos otras características. Por ejemplo, para
acceder a la documentación usamos --help.

$ python calculadora.py --help

usage: calculadora.py [-h] [-a NUMERO_A] [-b NUMERO_B] [-o {suma,resta,multiplicacion}]

Calculadora, suma/resta/multiplica a y b

optional arguments:

-h, --help show this help message and exit

-a NUMERO_A, --numero_a NUMERO_A

Parámetro a

-b NUMERO_B, --numero_b NUMERO_B

Parámetro b

-o {suma,resta,multiplicacion}, --operacion {suma,resta,multiplicacion}

Operación a realizar con a y b

Obtendremos un error si usamos un tipo incorrecto. Por ejemplo a no puede ser una cadena.

$ python calculadora.py -a=Hola -b=Hola --operacion=multiplicacion

usage: calculadora.py [-h] [-a NUMERO_A] [-b NUMERO_B] [-o {suma,resta,multiplicacion}]

calculadora.py: error: argument -a/--numero_a: invalid int value: 'Hola'


Por otro lado operacion sólo puede tomar los valores suma/resta/multiplicación, por lo que un
valor diferente dará error.

python calculadora.py -a=7 -b=3 -o=Hola

usage: calculadora.py [-h] [-a NUMERO_A] [-b NUMERO_B] [-o {suma,resta,multiplicacion}]

calculadora.py: error: argument -o/--operacion: invalid choice: 'Hola' (choose from 'suma', 'resta',
'multiplicacion')

Por último, el parámetro operacion es opcional, tomando el valor de suma por defecto.

$ python calculadora.py -a=7 -b=3

10

Como podemos observar el uso de type, help, choice, default y required nos ofrecen
funcionalidades extra que hacen que nuestra CLI sea más completa. Si bien es cierto que son los
más usados, Python ofrece otra muchas funcionalidades que vemos a continuación.

Usando abreviaciones

Es común soportar el uso de argumentos con - y -- como hemos visto anteriormente, siendo su uso
heredado de gnu. Normalmente se usa - para una abreviación, y -- para el nombre completo, siendo
el segundo el usado para nombrar a la variable que almacenará el argumento.

Por otro lado, Python permite usar abreviaciones del argumento automáticamente. Es decir, si
definimos --operacion, el argumento --oper funcionará correctamente.

# abreviaciones.py

import argparse

parser = argparse.ArgumentParser()

parser.add_argument('-o', '--operacion')

args = parser.parse_args()

print(vars(args))

Las siguiente llamadas son todas válidas y producen el mismo resultado.

$ python abreviaciones.py -o suma

{'operacion': 'suma'}

$ python abreviaciones.py --operacion suma


{'operacion': 'suma'}

$ python abreviaciones.py --opera suma

{'operacion': 'suma'}

$ python abreviaciones.py --op suma

{'operacion': 'suma'}

Cambiar el prefijo de los argumentos

Anteriormente hemos visto como por defecto los argumentos se pasan usando -, pero es posible
también cambiar el prefijo usando prefix_chars al crear el ArgumentParser.

# cambio_prefijo.py

import argparse

parser = argparse.ArgumentParser(prefix_chars='+')

parser.add_argument('+a')

parser.add_argument('+b')

args = parser.parse_args()

print(vars(args))

Ahora si queremos consultar la ayuda, deberemos usar el nuevo prefijo.

$ python cambio_prefijo.py +h

usage: cambio_prefijo.py [+h] [+a A] [+b B]

optional arguments:

+h, ++help show this help message and exit

+a A

+b B

Y podemos pasar parámetros de la siguiente manera.


$ python cambio_prefijo.py +a 10 +b 7

{'a': '10', 'b': '7'}

Pasando argumentos desde un archivo

Cuando trabajamos con un número muy elevado de argumentos, puede resultar interesante
moverlos a un fichero, lo que nos permitirá simplificar la llamada y hacerla más legible. ¿Te
imaginas pasar 20 argumentos por el terminal?. A continuación vemos un ejemplo donde los
argumentos se pasan dentro de un fichero, por lo que nuestro script sólo recibe el nombre del
fichero.

# argumentos_fichero.py

import argparse

parser = argparse.ArgumentParser(fromfile_prefix_chars='@')

parser.add_argument('-a')

parser.add_argument('-b')

parser.add_argument('-c')

parser.add_argument('-d')

parser.add_argument('-e')

parser.add_argument('-f')

args = parser.parse_args()

print(vars(args))

Asumiendo que tenemos el siguiente contenido almacenado en args.txt.

-a=3

-b=10

-c=1

-d=0

-e=11

-f=9
Podemos llamar a nuestro script de la siguiente forma, simplificando la llamada a un solo
argumento, lo que en ciertas ocasiones puede resultar mucho más elegante.

$ python argumentos_fichero.py @args.txt

{'a': '3', 'b': '10', 'c': '1', 'd': '0', 'e': '11', 'f': '9'}

Por último, es importante notar que el uso del fichero es una opción añadida, por lo que sigue
siendo posible utilizar los argumentos como hemos visto anteriormente, o incluso combinarlos,
siendo el último el que tiene prioridad.

$ python argumentos_fichero.py @args.txt -a 200

{'a': '200', 'b': '10', 'c': '1', 'd': '0', 'e': '11', 'f': '9'}

Argumentos excluyentes

Otra utilidad muy importante es la definición de grupos mutually exclusive o mutuamente


excluyentes. Su uso nos permite hacer que dos argumentos no puedan ser utilizados a la vez, es
decir, que sean excluyentes. Para ello creamos un grupo con add_mutually_exclusive_group y los
argumentos que añadamos al mismo no podrán ser usados simultáneamente.

# excluyentes.py

import argparse

parser = argparse.ArgumentParser()

grupo = parser.add_mutually_exclusive_group()

grupo.add_argument('-f', '--foo')

grupo.add_argument('-b', '--bar')

args = parser.parse_args()

print(vars(args))

La siguiente llamada por tanto no es posible, ya que estamos utilizando ambos argumentos.

$ python excluyentes.py -f 3 -b 10

usage: excluyentes.py [-h] [-f FOO | -b BAR]

excluyentes.py: error: argument -b/--bar: not allowed with argument -f/--foo

Usando acciones para los argumentos


Otra característica muy útil que nos ofrece argparse, es el uso de acciones personalizadas para
cada argumento utilizando action. Esto nos permite modificar como el argumento que
introducimos es parseado y almacenado en la variable, pudiendo usar acciones por defecto o
definirlas nosotros mismos.

Las acciones que argparse nos ofrece por defecto son las siguientes:

• store: Es el comportamiento por defecto que hemos visto anteriormente. Simplemente


almacena el valor que se pasa con el argumento en una variable.

• store_const: Almacena una constante en la variable, cuyo valor debemos especificar


en const.

• store_true: Almacena el booleano True en la variable. Muy útil para definir flags que no
reciben un valor concreto.

• store_false: Análogo al anterior pero almacena False.

• append: Añade el argumento a una lista. Útil cuando un argumento es pasado múltiples
veces.

• append_const: Similar a append pero almacena en la lista la constante especificada


en const.

• count: Cuenta el número de veces que un determinado argumento es pasado.

• help: Muestra la ayuda del programa y finaliza sin hacer nada más.

• version: Muestra la versión del programa y finaliza la ejecución.

Veamos unos ejemplos con cada tipo de acción. Como ya hemos visto anteriormente, el uso
de store no tiene ningún misterio ya que es el comportamiento que tenemos por defecto.

# store.py

import argparse

parser = argparse.ArgumentParser()

parser.add_argument('-a', action='store')

args = parser.parse_args()

print(vars(args))

Y podemos llamar a nuestro script como ya sabemos.

$ python store.py -a 3

{'a': '3'}

Por otro lado, store_const permite almacenar una constante determinada por el valor de const.

# store_const.py
import argparse

parser = argparse.ArgumentParser()

parser.add_argument('-a', action='store_const', const="99")

args = parser.parse_args()

print(vars(args))

La llamada al script se hace de la siguiente manera, pero nótese que el argumento no acepta
parámetros a continuación. Resulta lógico, puesto que ya estamos diciendo que queremos
almacenar const.

$ python store_const.py -a

{'a': '99'}

$ python store_const.py -a 1

usage: store_const.py [-h] [-a]

store_const.py: error: unrecognized arguments: 1

También podemos usar store_true para almacenar True en la variable o store_false para
almacenar False.

# store_true_false.py

import argparse

parser = argparse.ArgumentParser()

parser.add_argument('-a', action='store_true')

parser.add_argument('-b', action='store_false')

args = parser.parse_args()

print(vars(args))

Y podemos llamar al script de la siguiente manera.

$ python store_true_false.py -a -b

{'a': True, 'b': False}

$ python store_true_false.py -ab

{'a': True, 'b': False}


Por otro lado, podemos usar append para pasar el mismo argumento varias veces, siendo cada
valor almacenado en la misma lista.

# append.py

import argparse

parser = argparse.ArgumentParser()

parser.add_argument('-a', action='append')

args = parser.parse_args()

print(vars(args))

Pasamos el argumento -a tres veces con distintos valores y podemos ver como son almacenados
en una lista.

$ python append.py -a 1 -a 2 -a 3

{'a': ['1', '2', '3']}

Análogamente podemos usar append_const, que almacena el valor de const en la lista.

# append_const.py

import argparse

parser = argparse.ArgumentParser()

parser.add_argument('-a', action='append_const', const=0)

args = parser.parse_args()

print(vars(args))

Podemos llamarlo de la siguiente manera. Es importante notar que dado que el argumento no
acepta valores, podemos usar un solo -.

$ python append_const.py -a -a -a

{'a': [0, 0, 0]}

$ python append_const.py -aaa

{'a': [0, 0, 0]}

Por otro lado, el uso de count cuenta el número de veces que un argumento es pasado.

# count.py

import argparse

parser = argparse.ArgumentParser()
parser.add_argument('-a', action='count')

args = parser.parse_args()

print(vars(args))

Si pasamos múltiples veces el mismo argumento, la variable incrementará en uno cada vez.

$ python count.py -a -a -a

{'a': 3}

$ python count.py -aaa

{'a': 3}

Por último, podemos usar help y version para mostrar la ayuda del programa y su versión
respectivamente. Si bien es cierto que por defecto la ayuda se muestra con -h, esta funcionalidad
nos permite modificarlo, usando por ejemplo nombre en español.

# help_version.py

import argparse

parser = argparse.ArgumentParser()

parser.version = '1.0'

parser.add_argument('--ayuda', action='help')

parser.add_argument('--version', action='version')

args = parser.parse_args()

print(vars(args))

La siguiente llamada muestra la ayuda.

$ python help_version.py --ayuda

usage: help_version.py [-h] [--ayuda] [--version]

optional arguments:

-h, --help show this help message and exit

--ayuda

--version show program's version number and exit


Y de la siguiente manera podemos mostrar la versión, previa definición en el
campo version del parser.

$ python help_version.py --version

1.0

Hasta aquí hemos visto las acciones que Python nos ofrece por defecto, que deberían cubrir
prácticamente todos los casos de uso que se nos puedan ocurrir. Sin embargo, si necesitáramos
una funcionalidad que no existiera, siempre podríamos crear una acción personalizada creando
una clase que herede de argparse.Action. Una vez creada e implementada dicha clase, bastaría con
pasarla como action.

Argumentos con múltiples valores

Hasta ahora hemos visto que los valores que pasamos a los argumentos han sido o uno o ninguno,
pero es también posible que un argumento consuma múltiples valores. Los valores consumidos
pueden ser modificados con nargs y son:

• n: Se consumen n valores por el argumento, y se almacenen en una lista.

• ?: Se consume un sólo valor, que puede ser opcional.

• *: Para un número arbitrario de valores, y son almacenados en una lista.

• +: Similar a * pero requiere al menos un valor.

• argparse.REMAINDER: Consume todos los valores hasta el final.

Veamos unos ejemplos que es como sin duda se entiende mejor. Empecemos definiendo
un número concreto de valores a consumir.

# argumentos_variables1.py

import argparse

parser = argparse.ArgumentParser()

parser.add_argument('-a', nargs=5)

args = parser.parse_args()

print(vars(args))

Dado que el número de argumentos es fijo, la primera llamada es válida mientras que la segunda
no.

$ python argumentos_variables1.py -a 1 2 3 4 5

{'a': ['1', '2', '3', '4', '5']}

$ python argumentos_variables1.py -a 1 2
usage: argumentos_variables1.py [-h] [-a A A A A A]

argumentos_variables1.py: error: argument -a: expected 5 arguments

Para el caso de un sólo valor opcional tenemos el siguiente ejemplo.

# argumentos_variables2.py

import argparse

parser = argparse.ArgumentParser()

parser.add_argument('-a', nargs='?')

args = parser.parse_args()

print(vars(args))

El script puede ser llamado con o sin ningún valor, almacenando None si no se pasa ningún valor, y
por supuesto asumiendo que no se a definido un valor default.

$ python argumentos_variables2.py -a

{'a': None}

$ python argumentos_variables2.py -a 1

{'a': '1'}

También podemos pasar un número arbitrario de valores.

# argumentos_variables3.py

import argparse

parser = argparse.ArgumentParser()

parser.add_argument('-a', nargs='?')

args = parser.parse_args()

print(vars(args))

Todas las siguiente llamadas son válidas, ya que no se especifica un número concreto de
argumentos. Nótese que también puede no pasarse ningún argumento.

$ python argumentos_variables3.py -a

{'a': None}

$ python argumentos_variables3.py -a 1
{'a': ['1']}

$ python argumentos_variables3.py -a 1 2 3

{'a': ['1', '2', '3']}

Relacionado con el anterior, también podemos pasar un número arbitrario de valores con al menos
uno.

# argumentos_variables4.py

import argparse

parser = argparse.ArgumentParser()

parser.add_argument('-a', nargs='+')

args = parser.parse_args()

print(vars(args))

Es decir, podemos pasar un número arbitrario de parámetros pero deberemos pasar al menos uno,
por lo que la primera llamada falla.

$ python argumentos_variables4.py -a

usage: argumentos_variables4.py [-h] [-a A [A ...]]

argumentos_variables4.py: error: argument -a: expected at least one argument

$ python argumentos_variables4.py -a 1 2

{'a': ['1', '2']}

Por último si usamos argparse.REMAINDER se tomarán todos los valores restantes hasta el final del
comando. Dado que se toman todos los valores hasta el final, es importante declarar este
argumento al final de todo, de lo contrario podríamos tenemos comportamiento inesperados.

# argumentos_variables5.py

import argparse

parser = argparse.ArgumentParser()

parser.add_argument('-a')

parser.add_argument('-b', nargs=argparse.REMAINDER)

args = parser.parse_args()

print(vars(args))
$ python argumentos_variables5.py -a 10 -b 1 2 3 4 5

{'a': '10', 'b': ['1', '2', '3', '4', '5']}

Cambiar el nombre de la variable

Hasta ahora hemos visto como el nombre del argumento y el de la variable en la que se almacena
es el mismo, es decir, el argumento --operacion se almacena el la variable operacion por defecto.
Imaginemos que queremos que sea otro. Pues bien, es posible mediante el uso de dest.

# uso_dest.py

import argparse

parser = argparse.ArgumentParser()

parser.add_argument('-a', dest='otro_nombre')

args = parser.parse_args()

print(vars(args))

Como podemos ver, el argumento ya no es almacenado en la variable a, sino que se usará como
variable otro_nombre.

$ python uso_dest.py -a PythonMola

{'otro_nombre': 'PythonMola'}

Conclusiones

Llegados a este punto, ya hemos visto todas las funcionalidades que argparse nos ofrece. Recuerda
que si tienes dudas puedes consultar la documentación oficial de Python, que aunque sea más
complicada de leer, es la que debe ser usada como referencia.

Recapitulemos lo que hemos aprendido en este capítulo:

• Hemos visto qué son los Command Line Interface y su utilidad a la hora de ofrecer un punto
de entrada a nuestro código, algo ideal cuando queremos compartirlo con otras personas
para que lo usen.

• Hemos visto cómo se pueden crear CLI sencillos mediante el uso de la librería sys. Se trata
de una opción perfectamente válida pero cuya funcionalidad es muy limitada.

• Y por último, hemos visto como crear CLI con la librería nativa de Python argparse, el
estándar de facto usado, explorando desde las funcionalidades más básicas a las más
avanzadas.

Errores Comunes en Python


Código Buggy Python: Los 10 Errores más Comunes que Cometen los Desarrolladores Python

Código Buggy Python: Los 10 Errores más Comunes que Cometen los Desarrolladores Python

authors are vetted experts in their fields and write on topics in which they have demonstrated
experience. All of our content is peer reviewed and validated by Toptal experts in the same field.

ByMartin Chikilian

Martin is a full-stack engineer and has worked as a professional Python developer since 2007.

Previously At

Share this article

Translated by Marisela Ordaz. This article was originally written in English.

Acerca de Python

Python es un lenguaje de programación interpretado y orientado a objetos de alto nivel con


semántica dinámica. Su alto nivel integrado en las estructuras de datos, combinado con escritura y
binding dinámicos lo hacen muy atractivo para el desarrollo rápido de aplicaciones, así como para
su uso como lenguaje de script o glue para conectar componentes o servicios existentes. Python
trabaja con módulos y paquetes, fomentando así la modularidad del programa y la reutilización de
código.

Sobre este Artículo

La sintaxis simple y fácil de aprender de Python, puede enviar a los desarrolladores de Python en la
dirección incorrecta - especialmente aquellos que están conociendo la lengua – perdiendo en el
camino algunas de sus sutilezas y subestimando el poder del lenguaje diverso de Python.

Con esto en mente, este artículo presenta una lista “top 10” de errores sutiles, y difíciles de ver,
que pueden tomar desprevenidos incluso a algunos de los desarrolladores de Python más
avanzados.

(Nota: Este artículo está dirigido a un público más avanzado que el de Errores Comunes de
Programadores Python, que se orienta más hacia aquellos que son nuevos en la lengua.)

Error común # 1: Un Mal Uso de Expresiones como Valores Predeterminados para los Argumentos
de la Función

Python permite especificar que un argumento de la función es opcional, proporcionando un valor


por defecto para ello. Si bien ésta es una gran característica de la lengua, puede dar lugar a cierta
confusión cuando el valor por defecto es mutable. Por ejemplo, considera ésta definición de la
función de Python:

>>> def foo(bar=[]): # bar is optional and defaults to [] if not specified

... bar.append("baz") # but this line could be problematic, as we'll see...

... return bar

Un error común es pensar que el argumento opcional se establecerá en la expresión por defecto
específica, cada vez que se llama la función sin necesidad de suministrar un valor para el
argumento opcional. En el código anterior, por ejemplo, se podría esperar que llamar a foo() varias
veces (es decir, sin especificar un argumento bar) siempre daría de regreso baz, ya que la hipótesis
sería que cada vez que foo() se llama (sin un argumento bar especificado) bar está ajustado a [] (es
decir, una nueva lista vacía).

Pero vamos a ver lo que realmente sucede cuando se hace esto:

>>> foo()

["baz"]

>>> foo()

["baz", "baz"]

>>> foo()
["baz", "baz", "baz"]

¿Eh? ¿Por qué se siguió añadiendo el valor predeterminado de baz a una lista existente cada
vez que foo() era llamado, en lugar de crear una nueva lista en cada oportunidad? La respuesta más
avanzada de programación Python es que, el valor por defecto para un argumento de función se
evalúa sólo una vez, en el momento en que se define la función. Por lo tanto, el argumento bar se
inicializa con su valor por defecto (es decir, una lista vacía) sólo cuando foo() se ha definido por
primera vez, pero luego los llamados a foo() (es decir, sin un argumento bar especificado) seguirán
utilizando la misma lista a la que bar fue inicializado originalmente.

Por cierto, una solución común para esto es la siguiente:

>>> def foo(bar=None):

... if bar is None: # or if not bar:

... bar = []

... bar.append("baz")

... return bar

...

>>> foo()

["baz"]

>>> foo()

["baz"]

>>> foo()

["baz"]

Error común # 2: Uso Incorrecto de las Variables de Clase

Consideremos el siguiente ejemplo:

>>> class A(object):

... x=1

...

>>> class B(A):

... pass

...

>>> class C(A):

... pass
...

>>> print A.x, B.x, C.x

111

Tiene sentido.

>>> B.x = 2

>>> print A.x, B.x, C.x

121

Sí, de nuevo como se esperaba.

>>> A.x = 3

>>> print A.x, B.x, C.x

323

¿Qué es esto? Sólo cambiamos A.x ¿Por qué C.x cambió también?

En Python, las variables de clase se manejan internamente como diccionarios y siguen lo que se
refiere a menudo como Method Resolution Order (MRO). Así que en el código anterior, ya que el
atributo x no se encuentra en la clase C, se buscará en sus clases base (únicamente A en el ejemplo
anterior, aunque Python apoya herencia múltiple). En otras palabras, C no tiene su propia
propiedad x, independiente de A. Por lo tanto, las referencias a C.x son de hecho referencias a A.x.
Esto causa un problema Python, a menos que se maneje adecuadamente. Más información
sobre atributos de clase en Python.

Error común # 3: Especificación de Parámetros de Forma Incorrecta para un Bloque de Excepción

Supongamos que tienes el siguiente código:

>>> try:

... l = ["a", "b"]

... int(l[2])

... except ValueError, IndexError: # To catch both exceptions, right?

... pass

...

Traceback (most recent call last):

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

IndexError: list index out of range


El problema aquí es que el informe except no toma una lista de excepciones que se especifiquen de
esta forma. Por el contrario, Python 2.x la sintaxis except Exception, e, se utiliza para enlazar la
excepción al segundo parámetro opcional especificado (en éste caso e), con el fin de que esté
disponible para una inspección adicional. Como resultado, en el código anterior, la
excepción IndexError no está siendo capturada por el informe except; más bien, la excepción
termina siendo enlazada a un parámetro llamado IndexError.

La forma correcta de capturar varias excepciones en un informe except, es especificar el primer


parámetro como una tupla que contiene todas las excepciones a ser capturadas. Además, para la
máxima portabilidad, utiliza la palabra clave as, ya que la sintaxis es apoyada por Python 2 y Python
3:

>>> try:

... l = ["a", "b"]

... int(l[2])

... except (ValueError, IndexError) as e:

... pass

...

>>>

Error común # 4: No Entender las Reglas de Ámbito de Python

La resolución de ámbito de Python se basa en lo que se conoce como la regla LEGB, que es la
abreviatura de Local, Enclosing, Global, Built-in. Parece bastante sencillo, ¿verdad? Bueno, en
realidad, hay algunas sutilezas en la forma en que esto funciona en Python, lo que nos lleva al
problema común, más avanzado, de programación Python, a continuación.

Considera lo siguiente:

>>> x = 10

>>> def foo():

... x += 1

... print x

...

>>> foo()

Traceback (most recent call last):

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

File "<stdin>", line 2, in foo


UnboundLocalError: local variable 'x' referenced before assignment

¿Cuál es el problema?

El error anterior se debe a que, cuando se hace una asignación a una variable en un ámbito, esa
variable es considerada, automáticamente por Python, como local en ese ámbito y sigue cualquier
variable de nombre similar, en cualquier ámbito exterior.

Muchos de ellos son, por lo tanto, se sorprenden al conseguir un UnboundLocalError en el código


de trabajo anterior, cuando éste se modifica al añadir una instrucción de informe, en alguna parte
del cuerpo de una función. (Puedes leer más sobre esto aquí.)

Es particularmente común que esto confunda a los desarrolladores cuando usan listas. Considera
el siguiente ejemplo:

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

>>> def foo1():

... lst.append(5) # This works ok...

...

>>> foo1()

>>> lst

[1, 2, 3, 5]

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

>>> def foo2():

... lst += [5] # ... but this bombs!

...

>>> foo2()

Traceback (most recent call last):

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

File "<stdin>", line 2, in foo

UnboundLocalError: local variable 'lst' referenced before assignment

¿Eh? ¿Por qué foo2 falló, mientras que foo1 funcionó muy bien?

La respuesta es la misma que en el problema del ejemplo anterior, pero es sin duda más
sutil. foo1 No está haciendo una asignación a lst, mientras que foo2 sí lo está. Recordando que lst
+= [5] es en realidad la abreviatura de lst = lst + [5], vemos que estamos tratando de asignar un valor
a lst (por lo tanto, Python presume que está en el ámbito local). Sin embargo, el valor que estamos
tratando de asignar a lst se basa en el mismo lst (de nuevo, ahora presume estar en el ámbito local),
que aún no ha sido definido. Boom.

Error común # 5: Modificar una Lista al Iterar Sobre Ella

El problema con el siguiente código debería ser bastante obvio:

>>> odd = lambda x : bool(x % 2)

>>> numbers = [n for n in range(10)]

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

... if odd(numbers[i]):

... del numbers[i] # BAD: Deleting item from a list while iterating over it

...

Traceback (most recent call last):

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

IndexError: list index out of range

La eliminación de un elemento de una lista o matriz, mientras que se da iteración sobre ella, es un
problema de Python que es bien conocido por cualquier desarrollador de software con experiencia.
Sin embargo, aunque el ejemplo anterior puede ser bastante obvio, incluso los desarrolladores
avanzados pueden involuntariamente, ser tomados por sorpresa por éste código, el cual es mucho
más complejo.

Afortunadamente, Python incorpora una serie de paradigmas de programación elegantes que


cuando se utilizan correctamente, pueden resultar en un código significativamente simplificado y
racionalizado. Un beneficio secundario de esto es que el código más simple es menos probable
que sea sorprendido por el bug eliminación-accidental-de-un-item-de-lista-mientras-iterando-
sobre-ella. Uno de estos paradigmas es el de [comprensiones de
lista]((https://docs.python.org/2/tutorial/datastructures.html#tut-listcomps). Por otra parte, las
comprensiones de lista son particularmente útiles para evitar este problema específico, como se
muestra en ésta implementación alternativa del código mostrado arriba, que funciona
perfectamente:

>>> odd = lambda x : bool(x % 2)

>>> numbers = [n for n in range(10)]

>>> numbers[:] = [n for n in numbers if not odd(n)] # ahh, the beauty of it all

>>> numbers

[0, 2, 4, 6, 8]
Error común # 6: La Confusión de Cómo Python Enlaza las Variables en los Cierres

Teniendo en cuenta el siguiente ejemplo:

>>> def create_multipliers():

... return [lambda x : i * x for i in range(5)]

>>> for multiplier in create_multipliers():

... print multiplier(2)

...

Deberías esperar el siguiente resultado:

Pero en realidad obtienes:

¡Sorpresa!

Esto sucede debido al comportamiento enlace tardío de Python, que dice que los valores de las
variables utilizadas en los cierres, se buscan en el momento en el que se llama a la función interna.
Así que en el código anterior, cuando cualquiera de las funciones devueltas es llamada, el valor
de i se busca en el ámbito que lo rodea en el momento en que se llama (y en ese momento, el
círculo se ha completado, por lo que a i ya se le ha asignado su valor final de 4).

La solución a este problema común Python es un poco de hack:

>>> def create_multipliers():

... return [lambda x, i=i : i * x for i in range(5)]

...

>>> for multiplier in create_multipliers():


... print multiplier(2)

...

¡Voilà! Estamos tomando ventaja de los argumentos por defecto para generar funciones anónimas,
con el fin de lograr el comportamiento deseado. Algunos llamarían a esto, elegante. Algunos lo
llamarían sutil. Algunos lo odian. Pero si eres un desarrollador de Python, es importante entender
esto.

Error común # 7: Crear Dependencias de Módulos Circulares

Digamos que tienes dos archivos, a.py y b.py, cada uno de los cuales importa al otro, de la siguiente
manera:

en a.py:

import b

def f():

return b.x

print f()

Y en b.py:

import a

x=1

def g():

print a.f()

En primer lugar, vamos a tratar de importar a.py:

>>> import a
1

Funcionó muy bien. Tal vez te sorprendió. Después de todo, tenemos una importación circular aquí
que presumiblemente debería ser un problema, ¿no?

La respuesta es que la mera presencia de una importación circular no es como tal un problema en
Python. Si un módulo ya se ha importado, Python es lo suficientemente inteligente como para no
intentar volverlo a importar. Sin embargo, dependiendo del punto en el que cada módulo está
intentando acceder a las funciones o variables definidas en el otro, podrías tener problemas.

Así que volviendo a nuestro ejemplo, cuando importamos a.py, no tenía ningún problema al
importar b.py, ya que b.py no requiere nada de b.py para definirse en el momento de su
importación. La única referencia en b.py a a, es el llamado a a.f(). Pero ese llamado está en g() y
nada en a.py o b.py invoca g(). Así que, la vida es bella.

Pero ¿qué ocurre si se intenta importar b.py (claro, sin haber previamente importado a.py):

>>> import b

Traceback (most recent call last):

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

File "b.py", line 1, in <module>

import a

File "a.py", line 6, in <module>

print f()

File "a.py", line 4, in f

return b.x

AttributeError: 'module' object has no attribute 'x'

Uh oh. ¡Eso no es bueno! El problema aquí es que, en el proceso de importación b.py, intenta
importar a.py, lo que como resultado llama a f(), que a su vez intenta acceder a b.x. Pero b.x aún no
ha sido definida. De ahí la excepción AttributeError.

Al menos una solución a esto es bastante trivial. Simplemente modifica b.py para
importar a.py dentro de g():

x=1

def g():

import a # This will be evaluated only when g() is called

print a.f()
Cuando se importa, todo está bien:

>>> import b

>>> b.g()

1 # Printed a first time since module 'a' calls 'print f()' at the end

1 # Printed a second time, this one is our call to 'g'

Error común # 8: Choque de Nombres con Módulos de la Biblioteca Estándar de Python

Una de las ventajas de Python es la gran cantidad de módulos de biblioteca que trae desde el
principio. Pero como resultado, si no estás evitando esto conscientemente, no es tan difícil
encontrarse con un choque de nombres, entre el nombre de uno de tus módulos y un módulo con el
mismo nombre en la biblioteca estándar que se incluye en Python (por ejemplo, es posible que
tengas un módulo denominado email.py en tu código, lo que estaría en conflicto con el módulo de
biblioteca estándar del mismo nombre).

Esto puede conducir a problemas muy agresivos, como la importación de otra biblioteca que a su
vez intenta importar la versión de bibliotecas estándar Python de un módulo, pero como ya tienes
un módulo con el mismo nombre, el otro paquete importa erróneamente tu versión, en lugar de la
que se encuentra dentro de la biblioteca estándar Python, y es aquí es donde se producen los
errores más graves.

Por lo tanto, se debe tener cuidado para evitar el uso de los mismos nombres que los de los
módulos de biblioteca estándar Python. Es mucho más fácil para ti cambiar el nombre de un
módulo dentro de tu paquete que presentar una propuesta de mejora de Python (PEP) para solicitar
un cambio de nombre upstream y conseguir que lo aprueben.

Error común # 9: El No Poder Hacer Frente a las Diferencias entre Python 2 y Python 3

Considera el siguiente archivo foo.py:

import sys

def bar(i):

if i == 1:

raise KeyError(1)

if i == 2:

raise ValueError(2)

def bad():

e = None
try:

bar(int(sys.argv[1]))

except KeyError as e:

print('key error')

except ValueError as e:

print('value error')

print(e)

bad()

En Python 2, esto funciona muy bien:

$ python foo.py 1

key error

$ python foo.py 2

value error

Pero ahora vamos a dar un giro en Python 3:

$ python3 foo.py 1

key error

Traceback (most recent call last):

File "foo.py", line 19, in <module>

bad()

File "foo.py", line 17, in bad

print(e)

UnboundLocalError: local variable 'e' referenced before assignment

¿Qué acaba de ocurrir aquí? El “problema” es que en Python 3 el objeto de excepción no es


accesible más allá del alcance del bloque except. (La razón de esto es que, de lo contrario,
mantendría un ciclo de referencia con el marco de pila en la memoria hasta que se ejecute el
recolector de basura y purgue las referencias de la memoria. Más detalles técnicos sobre esto
están disponibles aquí).
Una forma de evitar este problema es mantener una referencia al objeto de excepción fuera del
alcance del bloque except, de modo que siga siendo accesible. Aquí hay una versión del ejemplo
anterior que utiliza esta técnica, por lo tanto, dosificando el código y haciéndole más compatible
con Python 2 y Python 3:

import sys

def bar(i):

if i == 1:

raise KeyError(1)

if i == 2:

raise ValueError(2)

def good():

exception = None

try:

bar(int(sys.argv[1]))

except KeyError as e:

exception = e

print('key error')

except ValueError as e:

exception = e

print('value error')

print(exception)

good()

La ejecución de éste es en Py3k:

$ python3 foo.py 1

key error

$ python3 foo.py 2
value error

¡Yupi!

(Por cierto, nuestra Guía de contratación Python (Python Hiring Guide) analiza una serie de
diferencias importantes, que se deben tener en cuenta al migrar el código de Python 2 a Python 3.)

Error común # 10: El Mal Uso del Método __del__

Digamos que tenías esto en un archivo llamado mod.py:

import foo

class Bar(object):

...

def __del__(self):

foo.cleanup(self.myhandle)

Y, luego, trataste de hacer esto desde another_mod.py:

import mod

mybar = mod.Bar()

Obtendrías una fea excepción AttributeError.

¿Por qué? Debido a que, como se informó aquí, al apagarse intérprete, las variables globales del
módulo se ajustan a None. Como resultado, en el ejemplo anterior, en el punto que __del__ se
invoca, el nombre foo ya se ha ajustado a None.

Una solución a este problema algo más avanzado que la programación Python, sería
utilizar atexit.register() en su lugar. De esta manera, cuando el programa se haya ejecutado (al salir
normalmente, quiero decir), tus gestores registrados son echados antes de que el intérprete se
apague.

Con éste conocimiento, una solución para el anterior código mod.py podría ser algo como esto:

import foo

import atexit

def cleanup(handle):

foo.cleanup(handle)
class Bar(object):

def __init__(self):

...

atexit.register(cleanup, self.myhandle)

Esta aplicación ofrece una manera limpia y confiable de llamar a cualquier funcionalidad de
limpieza necesaria para después de la terminación normal del programa. Obviamente, le toca a
foo.cleanup decidir qué hacer con el objeto unido al nombre self.myhandle, pero se entiende la
idea.

Para Terminar

Python es un lenguaje potente y flexible con muchos mecanismos y paradigmas que pueden
mejorar considerablemente la productividad. Sin embargo, al igual que con cualquier herramienta
de software o lenguaje, el tener una limitada comprensión o apreciación de sus capacidades a
veces puede ser más un impedimento que una ventaja, dejándonos en el estado proverbial de
“saber lo suficiente como para ser peligroso”.

Familiarizándose con los matices clave de Python, tales como (pero de ninguna manera se limita a)
los problemas de programación moderadamente avanzados planteados en este artículo, ayudará a
optimizar el uso de la lengua, evitando algunos de sus errores más comunes.

Deberías revisar nuestra Guía a Entrevistas Python (Insider’s Guide to Python Interviewing), para
obtener sugerencias sobre preguntas de entrevistas que pueden ayudar a identificar a los expertos
en Python.

Esperamos que te sean útiles los consejos en este artículo y apreciamos tus comentarios.

Código Pythonico

Si queremos escribir código Python que luzca profesional, debemos prestar atención a que nuestro
estilo sea “Pythónico”. Es decir, que aproveche las herramientas que el lenguaje provee para que
nuestros algoritmos sean más elegantes y simples.

En los tres ejemplos que se muestran vemos cómo el primero, si bien es sintácticamente correcto,
se parece a un algoritmo que podría haber sido escrito con algún otro lenguaje. Sin embargo, las
opciones 2 y 3 son propias de Python.

En la opción 2 se generan listas por comprensión donde los elementos son datos booleanos
correspondientes a cada carácter del string “s” e indican si ese carácter cumple o no con la
condición pedida (isalnum() verifica si es alfanumérico, isalpha() verifica si es
alfabético, isdigit() verifica si es un dígito, islower() verifica si es una letra minúscula
y isupper() verifica si es una letra mayúscula). Luego se imprime el resultado de la operación de
pertenencia usando el operador in.
La opción 3 también comienza de manera similar a la opción 2 pero aprovecha la función any(), que
devuelve True si alguno de los elementos de la secuencia que se le pasa como argumento es True.

Click aquí para una versión accesible de la infografía (apta para lectores electrónicos)


Compartir esto en →

Artículos relacionados

• ¡Cuidado con la concatenación implícita! (Categorías: python, c++)

• Un caso con generadores en Python (Categorías: python)

• Función zip en Python para iterar en simultáneo (Categorías: python)

• Pregunta de entrevista laboral (Python) (Categorías: python, ejercicios)

• Qué es yield en Python (Categorías: python, conceptos)

• Contar ocurrencias con collections.Counter() en Python (Categorías: python)

« Ámbito (o scope, en inglés)Conceptos básicos de SQL. Create, insert, select »

© Patricia E. Miguel.

Testing en Python

Dentro de la ingeniería software y la programación en general, el testing es una de las partes más
importantes que nos encontraremos en casi cualquier proyecto. De hecho es común dedicar más
tiempo a probar que el código funciona correctamente que a escribirlo. A continuación veremos las
formas más comunes de testear el código en Python, desde lo más básico a conceptos algo más
avanzados.

Tests Manuales y Tests Automatizados

Aunque sea la primera vez que leas acerca de testing en Python, estoy seguro de que ya has
ejecutado tests sobre tu código sin darte cuenta. De acuerdo a su forma de ejecución, los podemos
clasificar en:

• Tests manuales: Son tests ejecutados manualmente por una persona, probando diferentes
combinaciones y viendo que el comportamiento del código es el esperado. Sin duda los has
realizado alguna vez.

• Tests automáticos: Se trata de código que testea que otro código se comporta
correctamente. La ejecución es automática, y permite ejecutar gran cantidad de
verificaciones en muy poco tiempo. Es la forma más común, pero no siempre es posible
automatizar todo.
Imaginemos que hemos escrito una función que calcula la media de los valores que se pasan en
una lista como entrada.

def calcula_media(*args):

return(sum(*args)/len(*args))

A nadie se le ocurriría publicar nuestra función calcula_media sin haber hecho alguna verificación
anteriormente. Podemos por ejemplo probar con los siguientes datos y ver si la función hace lo que
se espera de ella. Al hacer esto ya estaríamos probando manualmente nuestro código.

print(calcula_media([3, 7, 5]))

# 5.0

print(calcula_media([30, 0]))

# 15.0

Con bases de código pequeñas y donde sólo trabajemos nosotros, tal vez sea suficiente, pero a
medida que el proyecto crece puede no ser suficiente. ¿Qué pasa si alguien modifica nuestra
función y se olvida de testear que funciona correctamente? Nuestra función habría dejado de
funcionar y nadie se habría enterado.

Es aquí donde los test automáticos nos pueden ayudar. Python nos ofrece herramientas que nos
permiten escribir tests que son ejecutados automáticamente, y que si fallan darán un error,
alertando al programador de que ha “roto” algo. Podemos hacer esto con assert, donde
identificamos dos partes claramente:

• Por un lado tenemos la llamada a la función que queremos testear, que devuelve un
resultado.

• Por otro lado tenemos el resultado esperado, que comparamos con el resultado devuelto por
la función. Si no es igual, se lanza un error.

assert(calcula_media([3, 7, 5]) == 5.0)

assert(calcula_media([30, 0]) == 15.0)

Nótese que los valores de 5 y 15 los hemos calculado manualmente, y corresponden con la media
de 3,7,5 y 30,0 respectivamente. Si por cualquier motivo alguien rompe nuestra
función calcula_media(), cuando los tests se ejecuten lanzaran una excepción.

Traceback (most recent call last):

File "ejemplo.py", line 7, in <module>

assert((calcula_media([30, 0]) == 15.0))

AssertionError
En proyectos grandes, es común que antes de permitirnos hacer merge de nuestro código
en master, se nos obligue a ejecutar un conjunto de tests automatizados. Si todos pasan, se
asumirá que nuestro código funciona y que no hemos “roto” nada, por lo que tendremos el visto
bueno.

Visto esto, tal vez pueda parecer que los test automatizados son lo mejor, sin embargo no siempre
se pueden automatizar los tests. Si por ejemplo estamos trabajando con interfaces de usuario, es
posible que no podamos automatizarlos, ya que se sigue necesitando a un humano que verifique
los cambios visualmente.

Tests Unitarios en Python con unittest

Aunque el uso de assert() puede ser suficiente para nuestros tests, a veces se nos queda corto y
necesitamos librerías como unittest, que ofrecen alguna que otra funcionalidad que nos hará la
vida más fácil. Veamos un ejemplo. Recordemos nuestra función calcula_media, que es la que
queremos testear.

# funciones.py

def calcula_media(*args):

return(sum(*args)/len(*args))

Podemos usar unittest para crear varios tests que verifiquen que nuestra función funciona
correctamente. Aunque la estructura de un conjunto de tests se puede complicar más, la
estructura será siempre muy similar a la siguiente:

• Creamos una clase Test<NombreDeLoQueSePrueba> que hereda de unittest.TestCase.

• Definimos varios tests como métodos de la clase, usando test_<NombreDelTest> para


nombrarlos.

• En cada test ejecutamos las comprobaciones necesarias, usando assertEqual en vez


de assert, pero su comportamiento es totalmente análogo.

# tests.py

from funciones import calcula_media

import unittest

class TestCalculaMedia(unittest.TestCase):

def test_1(self):

resultado = calcula_media([10, 10, 10])

self.assertEqual(resultado, 10)
def test_2(self):

resultado = calcula_media([5, 3, 4])

self.assertEqual(resultado, 4)

if __name__ == '__main__':

unittest.main()

Si ejecutamos el código anterior, obtendremos el siguiente resultado. Esta es una de las ventajas
de unittest, ya que nos muestra información sobre los tests ejecutados, el tiempo que ha tardado y
los resultados.

Ran 2 tests in 0.006s

OK

Por otro lado, usando -v podemos obtener más información sobre cada test ejecutado con su
resultado individualmente. Si tenemos gran cantidad de tests suele ser recomendable usarla, ya
que será más fácil localizar los tests que han fallado.

$ python -m unittest -v tests

test_1 (tests.TestCalculaMedia) ... ok

test_2 (tests.TestCalculaMedia) ... ok

----------------------------------------------------------------------

Ran 2 tests in 0.000s

OK

Por último, si tenemos varios ficheros de test, podemos usar discover, para decirle a Python que
busque en la carpeta todos los tests y los ejecute.

$ python -m unittest discover -v

Otras comprobaciones en unittest

Anteriormente hemos visto el uso de .assertEqual(a, b) que simplemente verifica que dos
valores a y b son iguales, y de lo contrario da error. Sin embargo unittest nos ofrece un amplio
abanico de opciones. Nótese que existen algunas variaciones usando “not”, como assertNotIn():
• .assertEqual(a, b): Verifica la igualdad de ambos valores.

• .assertTrue(x): Verifica que el valor es True.

• .assertFalse(x): Verifica que el valor es False.

• .assertIs(a, b): Verifica que ambas variables son la misma (ver operador is).

• .assertIsNone(x): Verifica que el valor es None.

• .assertIn(a, b): Verifica que a pertenece al iterable b (ver operador in).

• .assertIsInstance(a, b): Verifica que a es una instancia de b

• .assertRaises(x): Verifica que se lanza una excepción.

import unittest

class TestEjemplos(unittest.TestCase):

def test_in(self):

# Ok ya que 1 esta contenido en [1, 2, 3]

self.assertIn(1, [1, 2, 3])

def test_is(self):

a = [1, 2, 3]

b=a

# Ok ya que son el mismo objeto

self.assertIs(a, b)

def test_excepcion(self):

# Dividir 0/0 da error, pero es lo esperado por el test

with self.assertRaises(ZeroDivisionError):

x = 0/0

$ python -m unittest -v tests

test_excepcion (tests.TestEjemplos) ... ok

test_in (tests.TestEjemplos) ... ok

test_is (tests.TestEjemplos) ... ok


----------------------------------------------------------------------

Ran 3 tests in 0.000s

OK

Usando setUp y tearDown

Otra de las ventajas de usar unittest, es que nos ofrece la posibilidad de definir funciones comunes
que son ejecutadas antes y después de cada test. Estos métodos son setUp() y tearDown().

import unittest

class TestEjemplos(unittest.TestCase):

def setUp(self):

print("Entra setUp")

def tearDown(self):

print("Entra tearDown")

def test_1(self):

print("Test: test_1")

def test_2(self):

print("Test: test_2")

Siendo el resultado el siguiente. Podemos ver que hace una especie de sandwich con cada test,
metiéndolo entre setUp y tearDown. Nótese que si ambas funciones realizan siempre lo mismo, tal
vez se pueda usar un TestSuite con una función común para todos los tests, pero se trata de un
concepto algo más avanzado que dejaremos para otro artículo.

$ python -m unittest -v tests

test_1 (tests.TestEjemplos) ... Entra setUp

Test: test_1

Entra tearDown

ok

test_2 (tests.TestEjemplos) ... Entra setUp


Test: test_2

Entra tearDown

ok

----------------------------------------------------------------------

Ran 2 tests in 0.000s

OK

Evitando Side Effects

Hasta ahora hemos visto las herramientas que necesitamos para escribir nuestros tests, pero es
también muy importante seguir una serie de buenas practicas a la hora de escribir nuestro código.
Uno de los principios más importantes a seguir es el Principio de Responsabilidad Única o SRP, que
nos dice que el código (bien sea una clase o una función) debe tener una única responsabilidad. Si
hace demasiadas cosas, será más complicado de modificar y mantener, y además será más
complicado de testear.

Por lo tanto es tan importante escribir buenos tests que sean completos y tengan en cuenta todas
las posibles casuísticas como escribir código que pueda ser testeado de manera fácil.

Top 50 Ejemplos y Ejercicios

Ahora que ya dominas Python veamos unos ejemplos prácticos en diferentes sectores:

• � Inteligencia artificial, redes neuronales, genética y ADN.

• � Dashboards y representación gráfica.

• � Análisis financiero, apuestas, estadística y matemáticas.

• � Encriptado, firma digital, computación cuántica, factorización de números primos y


fuerza bruta.

• � Desarrollo de juegos, webs, APIs e interfaces de usuario.

• � Implementación de algoritmos sencillos.

Estos ejemplos dar ideas de lo que se puede hacer con Python, dejando al lector unos ejercicios
para que explore más allá.

Aquí puedes encontrar los códigos completos:

• � github.com/ellibrodepython/50-ejemplos-python

• 📙📙 Firma digital con cryptography


• 📙📙 Encriptar mensaje con cryptography

• 📙📙 Determina si un número es primo

• 📙📙 Factoriza un número en primos

• 📙📙 Computación cuántica con qiskit

• 📙📙 Fuerza bruta con itertools

• 📙📙 Acortador de enlaces con flask

• 📙📙 Construye una API con flask

• 📙📙 Trabaja con ficheros con os

• 📙📙 Bases de datos con sqlite

• 📙📙 Pattern matching con re

• 📙📙 Analítica de futbol con seaborn

• 📙📙 Programar tareas

• 📙📙 Gráficas con matplotlib

• 📙📙 Redes neuronales con tensorflow

• 📙📙 Logging y verbosity

• 📙📙 Cálculo simbólico con sympy

• 📙📙 Crear juego con pygame

• 📙📙 Scrapping web con beautifulsoup

• 📙📙 Unir pdfs pdf

• 📙📙 Excels con pyexcel

• 📙📙 Benchmark de funciones

• 📙📙 Rotar imagen con scikit-image

• 📙📙 Simulaciones simpy

• 📙📙 Coordenadas y distancia con geopy

• 📙📙 Genética y ADN con biopython

• 📙📙 Moléculas con rdkit

• 📙📙 Polinomios con numpy


• 📙📙 Simular apuestas con numpy

• 📙📙 Monte Carlo con numpy

• 📙📙 Análisis financiero con yfinance

• 📙📙 Programación asincrona con aiohttp

• 📙📙 Baraja de Poker

• 📙📙 Barajar cartas con shuffle

• 📙📙 Ordenar con bubble sort

• 📙📙 Convertir binario a decimal

• 📙📙 Código C en Python

• 📙📙 Martingala y apuestas

• 📙📙 Temporizador con time

• 📙📙 Calcular impuestos

• 📙📙 Plot interactivo con bokeh

• 📙📙 Simular hipoteca

• 📙📙 El Quijote con wordcloud

• 📙📙 Programas ejecutables con pyinstaller

• 📙📙 Calcular interés compuesto

• 📙📙 Busca el número que falta

• 📙📙 Comprimir información

• 📙📙 Interfaz de usuario con pyqt

• 📙📙 Dashboard con streamlit

• 📙📙 Problema de la mochila

También podría gustarte