Manual Python
Manual 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.
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.
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'>
Son las siguientes, y aunque alguna pueda parecer lógica, a veces resultan no serlo tanto cuando
no se cumplen:
¿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.
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!
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.
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.
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.
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.
MÉ TO D O 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É TO D O 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.
python3 -V
apt-get update
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:
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 + .
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.
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
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.
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.
# Si imprimir, print()
if imprimir:
print(x, d)
# Salida: El valor de (a+b)*c es 14
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
'''
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.
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)
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
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.
# 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
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)
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.
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
# No válido
2variable = 10
var-iable = 10
var iable = 10
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)
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)
Vistas ya las palabras reservadas de Python, a continuación explicaremos para que sirve cada una
de ellas y las pondremos en contexto.
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")
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
x = (5 == 1)
print(x)
# Salida: False
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
funcion_suma(3, 5)
# Salida: La suma es 8
Si queremos que la función devuelva uno o varios valores, podemos usar return .
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.
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()
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
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
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.
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
a = 10
del a
print(a)
import asyncio
asyncio.run(main())
# Salida:
# Empieza proceso: 1
# Empieza proceso: 2
# Empieza proceso: 3
# Acaba proceso: 1
# Acaba proceso: 2
# Acaba proceso: 3
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()
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.
def imprimir_mensaje():
print(mensaje) # Accediendo a la variable global
imprimir_mensaje()
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}")
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.
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.
🔄 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:
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}")
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}")
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.
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
)''')
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)
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)
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 .
Unpacking en Python
El unpacking en Python nos permite asignar una lista a múltiples variables en una única línea de
código.
a, b, c = [1, 2, 3]
print(a) # 1
print(b) # 2
print(c) # 3
a, b, c = (1, 2, 3)
print(a) # 1
print(b) # 2
print(c) # 3
Y como es de esperar, el número de variables debe coincidir con la longitud. Obtenemos not
enough values to unpack si proporcionamos menos.
a, b, c = (1, 2)
# ValueError: not enough values to unpack (expected 3, got 2)
a, b = (1, 2, 3, 4)
# ValueError: too many values to unpack (expected 2)
Se pueden dar casos curiosos como el siguiente, ya que en realidad funciona con
cualquier iterable.
a, b, c = "123"
print(a) # 1
print(b) # 2
print(c) # 3
De hecho funciona también con diccionarios, siendo la key lo usado por defecto.
a, b, c = range(3)
print(a) # 0
print(b) # 1
print(c) # 2
Operador de Unpacking
Relacionado con el unpacking existe el operador * , que nos permite realizar asignaciones
cuando el número de elementos es distinto. Tanto de esta manera.
*a, b = (1, 2, 3)
print(a) # [1, 2]
print(b) # 3
a, *b = (1, 2, 3)
print(a) # 1
print(b) # [2, 3]
Podemos usarlo para unir listas. Aunque es importante notar que esto se puede hacer de otras
formas como usando + o .extend() .
a = [1, 2]
b = [3, 4]
c = [*a, *b]
print(c)
# [1, 2, 3, 4]
a = {"uno": 1, "dos": 2}
b = {"tres": 3, "cuatro": 4}
c = {**a, **b}
print(c)
# {'uno': 1, 'dos': 2, 'tres': 3, 'cuatro': 4}
a = {"uno": 1, "dos": 2}
b = {"uno": 0, "dos": 0}
c = {**a, **b}
print(c)
# {'uno': 0, 'dos': 0}
Y por último también podemos hacer cosas interesantes con bucles for.
# Primero: 1
# Resto: [2, 3]
# Primero: 4
# Resto: [5, 6, 7]
a, b = (1, 2)
print(a, b)
#12
a, b = b, a
print(a, b)
#21
Unpacking en Funciones
Por último, aunque lo vemos más en detalle en args y kwargs, el unpacking nos permite pasar un
número de argumentos variables a una función.
funcion(1)
# args=1 args=() kwargs={}
funcion(1, 2)
# args=1 args=(2,) kwargs={}
funcion(1, 2, 3, cuatro=4, cinco=5)
# args=1 args=(2, 3) kwargs={'cuatro': 4, 'cinco': 5}
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?
# 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.
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.
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 ; .
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() .
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.
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.
# 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.
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
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.
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.
# Salida: 45
Range en Python
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.
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 .
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:
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.
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.
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
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.
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")
else:
print("x")
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.
En el siguiente:
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
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"
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
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.
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.
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
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.
#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.
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
La función zip() de Python viene incluida por defecto en el namespace, lo que significa que
puede ser usada sin tener que importarse.
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)
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)
# 1 Uno One Un
# 2 Dos Two Deux
numeros = [1, 2, 3, 4, 5]
espanol = ["Uno", "Dos"]
# 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.
numeros = [1, 2, 3, 4, 5]
zz = zip(numeros)
print(list(zz))
Si realizamos lo siguiente, a,b toman los valores de las key del diccionario. Tal vez algo no
demasiado interesante.
#11
#22
#33
Sin embargo, si hacemos uso de la función items, podemos acceder al key y value de cada
elemento.
# 1 Uno One
# 2 Dos Two
# 3 Tres Three
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')]
print(a) # (1, 2, 3)
print(b) # ('One', 'Two', 'Three')
Enumerate en Python
El uso del for en Python nos permite iterar colecciones, recorriendo todos los elementos de la
misma.
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.
indice = 0
for l in lista:
print(indice, l)
indice += 1
# Salida:
#0A
#1B
#2C
# 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.
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.
1. 🏄🏻♀️02. Estructuras de control
2. 📙 List comprehensions
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
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.
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.
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
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.
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.
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á.
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.
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.
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.
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.
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.
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.
# 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.
📗 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))
#30549363634996046820519793932136176997894027405723266638936139092812916265247
204577018572351080152282568751526935904671553178534278042839697351331142009178
896307244205337728522220355888195318837008165086679301794879136633899370525163
649789227021200352450820912190874482021196014946372110934030798550767828365183
620409339937395998276770114898681640625000000000000000000000000000000000000000
000000000000000000000000000000000000000000000000000000000000000000000000000000
000000000000000000000000000000000000000000000000000000000000000000000000000000
0000000000000000000000000000000000000000000000000000000
#<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 .
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.
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
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í.
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?
En pocas palabras, los números complejos son aquellos que tienen dos partes:
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
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.
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.
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)
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.
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
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).
También es valido declarar las cadenas con comillas simples simples ' .
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.
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.
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.
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.
Es posible también darle nombre a cada elemento, y format() se encargará de reemplazar todo.
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
a = 5; b = 10
s = f"a + b = {a+b}"
print(s) #a + b = 15
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
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'>
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])
x = "abcde"
print(x[2:])
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.
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 = "[email protected]"
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.
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.
lista = [1, 2, 3, 4]
lista = list("1234")
Una lista sea crea con [] separando sus elementos con comas , . Una gran ventaja es que
pueden almacenar tipos de datos distintos.
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.
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]
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.
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.
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.
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]
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 .
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.
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'>
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
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.
tupla = (1, 2, 3)
print(tupla) #(1, 2, 3)
tupla = 1, 2, 3
print(type(tupla)) #<class 'tuple'>
print(tupla) #(1, 2, 3)
tupla = (1, 2, 3)
#tupla[0] = 5 # Error! TypeError
Al igual que las listas, las tuplas también pueden ser anidadas.
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
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
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.
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'}
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}
Iterar diccionario
Los diccionarios se pueden iterar de manera muy similar a las listas u otras estructuras de datos.
Para imprimir los key .
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.
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}
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.
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
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.
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
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:
a = 3.5
a = int(a)
print(a)
# Salida: 3
a = 3.5
print(type(a)) # <class 'float'>
a = str(a)
print(type(a)) # <class 'str'>
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'
a = "3"
print(type(a)) # <class 'str'>
a = int(a)
print(type(a)) # <class 'int'>
a = "Python"
a = int(a)
# ValueError: invalid literal for int() with base 10: 'Python'
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)
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)
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.
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
# 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:
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:
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.
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.
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).
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]
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.
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.
# 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
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)
unciones 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
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.
def di_hola():
print("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.
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.
resta(a=3, b=5) # -2
resta(b=5, a=3) # -2
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.
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
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
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
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:
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.
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.
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)
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.
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'
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
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.
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.
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.
funcion(x)
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.
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
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.
Veamos un ejemplo.
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.
args = [1, 2, 3, 4]
kwargs = {'x':"Hola", 'y':"Que", 'z':"Tal"}
Anotaciones en Funciones
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.
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 [] .
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
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.
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.
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.
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.
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.
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
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
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.
Al igual que en las funciones se puede tener un número variable de argumentos haciendo uso
de * , lo conocido como tuple unpacking.
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.
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.
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 .
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
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.
@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:
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
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 @ .
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.
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()
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
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.
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.
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.
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
Caching de Funciones
def es_primo(num):
for n in range(2, num):
if num % n == 0:
return False
return True
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
@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()
¿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:
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
print(list(lista_pordos))
# [2, 4, 6, 8, 10]
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.
print(list(pares))
# [4, 16, 8]
def es_par(x):
return x % 2 == 0
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.
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.
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.
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:
Tal vez no muy legible, pero todo lo anterior se podrá expresar en una única línea de código.
📗 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
= 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 = 1
|= x|=2 x=x|2 = 7
^= x^=2 x=x^2 = 5
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.
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.
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.
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.
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 .
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 .
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.
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 +
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
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.
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
== Igual x == y = False
!= Distinto x != 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
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.
and Devuelve True si ambos elementos son True True and True = True
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) .
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 .
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
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 .
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á.
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
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
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
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
print(bin(27))
# 0b11011
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
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.
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 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 .
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
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
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]
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;
}
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.
De manera similar, podemos reutilizar el resultado de una expresión evitando tener que volver a
computarla.
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.
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.
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.
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.
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.
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 .
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'
# 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
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'
# Atributos de instancia
self.nombre = nombre
self.raza = raza
def ladra(self):
print("Guau")
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.
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:
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"
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
mi_clase = Clase()
mi_clase.metodo("a", "b")
# ('Método normal', <__main__.Clase at 0x10b9daa90>)
class Clase:
@classmethod
def metododeclase(cls):
return 'Método de clase', cls
Clase.metododeclase()
# ('Método de clase', __main__.Clase)
mi_clase.metododeclase()
# ('Método de clase', __main__.Clase)
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
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.
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.
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
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.
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:
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'>)
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
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 () .
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'
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”.
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.
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")
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.
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 .
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.
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.
@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")
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")
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.
class FloatABC(metaclass=ABCMeta):
pass
FloatABC.register(float)
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 .
class Clase(ABC):
@classmethod
@abstractmethod
def metodo_abstracto_de_clase(cls):
pass
class Clase(ABC):
@staticmethod
@abstractmethod
def metodo_abstracto_estatico():
pass
class Clase(ABC):
@property
@abstractmethod
def metodo_abstracto_propiedad(self):
pass
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.
def __iter__(self):
return iter(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")
Abstracción en programación
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.
Es posible crear métodos abstractos en Python con decoradores como @absttractmethod , pero
esto lo dejamos para otro post.
Herencia
Cohesión
Abstracción
Polimorfismo
Acoplamiento
Encapsulamiento
Acoplamiento en programación
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
Herencia
Cohesión
Abstracción
Polimorfismo
Acoplamiento
Encapsulamiento
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
class MiClase:
atributo1 = "valor1"
atributo2 = "valor2"
class MiClase:
atributo1 = "valor1"
atributo2 = "valor2"
def __init__(self, argumento1):
self.argumento1 = argumento1
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")
mi_clase.atributo1
mi_clase.atributo2
mi_clase.argumento1
mi_clase.funcion1()
Encapsulamiento en programación
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
# '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
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']
mi_clase._Clase__atributo_clase
# 'Hola'
mi_clase._Clase__mi_metodo()
# 'Haz algo'
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() {}
}
// Código Java
class Perro extends Animal {
public Perro() {}
}
// 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();
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.
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!")
# 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
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.
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.
Herencia
Cohesión
Abstracción
Polimorfismo
Acoplamiento
Encapsulamiento
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.
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.
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.
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.
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!")
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))
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")
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")
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")
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.
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
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() .
# 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)
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.
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))
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:
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.
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.
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.
#<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 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"])
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.
¿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.
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.
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:
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()
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.
Por supuesto se trata de un ejemplo didáctico, si quieres leer un fichero simplemente usa las
funciones que Python proporciona por defecto.
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 .
@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!")
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
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.
# 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
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.
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.
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.
## 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.
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.
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.
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.
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.
Método write()
Ya hemos visto como crear el fichero. Veamos ahora como podemos añadir contenido.
Empecemos escribiendo un texto.
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')
# 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.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.writelines(lista)
fichero.close()
# Se guarda
# Manzana
# Pera
# Plátano
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_fichero("Esto es un mensaje")
print(lee_ficher
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:
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.
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:
$ 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
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.
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
Visto ya un ejemplo concreto, a continuación veremos las normas más importantes introducidas
en la PEP8.
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
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.
# Correcto
x=5
# Incorrecto
x=5
# 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)
def duplica(a):
return a * 2
# Correcto
duplica(2)
# Incorrecto
duplica( 2 )
# 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('...')
# 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 ]
# 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
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:
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)
# 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.
Sin embargo, con un pequeño cambio, podemos cambiar la forma en la que se codifica el texto.
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:
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.
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:
# 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))
# 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 organización, debiendo haber una línea de separación entre cada grupo:
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 *
# Correcto
from collections import deque, defaultdict
# 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.
# Incorrecto
x = x + 1 # Suma 1 a la variable x
# Correcto
x = x + 1 # Compensa el offset producido por la medida
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:
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()
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.
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
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
class Clase:
def __init__(self):
print("Init")
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
$ ls -ta
$ mkdir -p foo
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.
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
# 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.
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.
# 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.
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.
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.
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.
Poniéndolo todo junto en nuestro programa calculadora.py , tendríamos algo como lo siguiente.
# calculadora.py
import argparse
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.
Una vez vistas como se realizarían las llamadas, veamos otras características. Por ejemplo, para
acceder a la documentación usamos --help .
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.
Por otro lado operacion sólo puede tomar los valores suma/resta/multiplicación, por lo que un
valor diferente dará error.
Por último, el parámetro operacion es opcional, tomando el valor de suma por defecto.
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))
# 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))
$ python cambio_prefijo.py +h
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'}
# 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))
-a=3
-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
Las acciones que argparse nos ofrece por defecto son las siguientes:
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))
$ 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))
$ python store_true_false.py -a -b
{'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']}
# 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]}
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}
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))
optional arguments:
-h, --help show this help message and exit
--ayuda
--version show program's version number and exit
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_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
# 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'}
# 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']}
# 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 .
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.
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.
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: 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.
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.
# 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.
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.
----------------------------------------------------------------------
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.
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
----------------------------------------------------------------------
Ran 3 tests in 0.000s
OK
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.
----------------------------------------------------------------------
Ran 2 tests in 0.000s
OK
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.
Ejerci o
Veamos como proteger la información para asegurarnos de que el autor es quien dice ser.
Antiguamente se usaban las firmas con pluma. Cada persona tenía una firma reconocible y difícil
de copiar.
Actualmente tenemos la firma digital donde la integridad de la información está asegurada por la
criptografía. La firma digital es un sistema criptográfico donde existen dos claves relacionadas
matemáticamente:
Con la firma digital podemos verificar el origen o integridad de un mensaje. Si firmas un mensaje
con tu clave privada, cualquiera podrá verificar su autenticidad con tu clave pública.
Veamos un ejemplo donde firmamos un mensaje con tu clave privada. La clave privada es el
equivalente a la pluma y a tu firma, algo que sólo tú puedes crear. Empezamos importando los
módulos que necesitamos.
Podemos crear una clave privada de la siguiente manera. La clave pública deriva de la privada.
privada = ec.generate_private_key(ec.SECP256R1())
publica = privada.public_key()
Puedes ver la privada así. En aplicaciones reales no debes enseñar esta clave a nadie, o se
podrían hacer pasar por ti.
pem = privada.private_bytes(
serialization.Encoding.PEM,
serialization.PrivateFormat.PKCS8,
serialization.NoEncryption())
Podemos firmar un mensaje utilizando sign . Esto es algo muy usado en transacciones
blockchain. Esta firma permite reconocer que el creador de ese mensaje es quien tiene la clave
privada. Haciendo esto es como si autorizaras esta transacción.
firma = privada.sign(
b"Envia 1 Euro al IBAN 123",
ec.ECDSA(hashes.SHA256()))
Ahora, un tercero podría verificar que el mensaje fue efectivamente creado por el conocedor
de privada . Para esto sólo se necesita publica . Podemos verificar que el mensaje no ha sido
alterado.
try:
publica.verify(
firma,
b"Envia 1 Euro al IBAN 123",
ec.ECDSA(hashes.SHA256()))
print("Verificacion OK")
except InvalidSignature:
print("Verificacion NOK")
# Verificacion OK
Si ahora intentamos verificar la firma con verify obtendremos un error InvalidSignature . Nos
hemos protegido de un atacante modificando el mensaje.
try:
publica.verify(
firma,
b"Envia 100 Euros al IBAN 456",
ec.ECDSA(hashes.SHA256()))
print("Verificacion OK")
except InvalidSignature:
print("Verificacion NOK")
# Verificacion NOK
En pocas palabras, la firma digital permite evitar que alguien suplante tu identidad y diga cosas
que tú no has dicho. Eso sí, siempre y cuando la clave privada solo la conozcas tú.
✏️Ejercicios:
Intenta usar otra clave pública distinta para verificar la firma con verify y explica lo que
sucede.
Veamos como encriptar un mensaje para que solo el destinatario autorizado pueda
desencriptarlo y verlo. Existen dos formas de hacer esto.
⚖️Con criptografía simétrica. En este caso la clave para encriptar y desencriptar son la
misma. Ya los Romanos usaban este tipo de criptografía.
🔀 Con criptografía asimétrica. Existen dos claves diferentes. Una para encriptar y otra para
desencriptar.
A continuación veremos como realizarlo con criptografía asimétrica. Como hemos indicado,
existen dos claves:
Una clave pública que cualquier puede conocer. Se usa para encriptar.
Una clave privada 🔑 que debe mantenerse en secreto. Se usa para desencriptar.
Importamos lo necesario.
privada = rsa.generate_private_key(
public_exponent=65537,
key_size=2048)
publica = clave_privada.public_key()
desencriptado = privada.decrypt(
encriptado,
padding.OAEP(
mgf=padding.MGF1(algorithm=hashes.SHA256()),
algorithm=hashes.SHA256(),
label=None))
print(mensaje_des)
# b'Mensaje secreto de El Libro de Python'
Gran parte de la criptografía actual funciona gracias a lo difícil que es factorizar números en sus
primos. Técnicamente alguien podría desencriptar tu mensaje sin la clave privada, probando
múltiples combinaciones. Sin embargo esto no es posible en la práctica, ya que se tardaría un
tiempo infinito. A nivel práctico, lo consideramos imposible.
Cierto es que los ordenadores cuánticos pueden romper los esquemas tradicionales de
encriptación, pero la criptografía post-cuántica resuelve esto. Aún quedan unos años para que
esto sea una realidad.
✏️Ejercicios:
Intenta desencriptar el mensaje con otra clave privada distinta y explica lo que sucede.
Anterior🕺🏻 01. Introducción
Siguiente
def es_primo(n):
if n <= 1:
return False
for i in range(2, n):
if n % i == 0:
return False
return True
Aunque la anterior función es correcta existe una optimización. No es necesario probar a dividir
todos los números. Basta con probar hasta sqrt(n) + 1 . En otras palabras, no hace falta probar
si 10 , 11 , 12 dividen a 13 . Ya sabes que no porque son mayores que 4 . Sabiendo esto
podemos optimizar el código.
def es_primo(n):
if n <= 1:
return False
for i in range(2, int(n**0.5) + 1):
if n % i == 0:
return False
return True
q = 41
print(f"{p} es primo? {es_primo(p)}")
✏️Ejercicios:
Escribe una función que busque un número primo muy grande, con 8 dígitos.
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.
🔄 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:
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}")
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}")
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.