Python
Python
• Elementos
• Semántica del lenguaje
• Estructuras de Control de Flujo
• Tipos de datos
▪ Tipos secuencia (str, tuple, range, list)
▪ Textos (str)
▪ Tuplas (tuple)
▪ Rangos (ranges)
▪ Listas (list)
▪ Tipos conjunto (set, frozenset)
▪ Tipos Mapa (dict)
▪ Colecciones avanzadas
▪ Otras estructuras de datos de interés
▪ Tipado dinámico
▪ Asignación en Python
▪ Compresiones de listas, conjuntos y diccionarios
• Funciones
• Módulos, paquetes y espacios de nombres
• Errores y Excepciones
• Ficheros y Sistema Operativo
• Expresiones Regulares
• Usando el intérprete de Python
• Código Máquina
code
00001000 00000010 01111011 10101100 10010111 11011001 01000000
01100010
00110100 00010111 01101111 10111001 01010110 00110001 00101010
00011111
10000011 11001101 11110101 01001110 01010010 10100001 01101010
00001111
11101010 00100111 11000100 01110101 11011011 00010110 10011111
01010110
• Ensamblador
• Alto nivel
print("Hello, World")
Lenguaje interpretado
Tanto compiladores como interpretes son programas que convierten el código que escribes a
lenguaje de máquina.
Crea aplicaciones multiplataforma mientras el Se deben crear diferentyes versiones para los
SO cuente con un intérprete diferentes SO
Suele corresponder con Open Source Suele corresponder con entornos propietarios
Los lenguajes de programación de tipo estático son aquellos en los que es necesario definir las
variables antes de su uso y asignarles el tipo de contenido al que refernciarán. Esto implica que
la tipificación estática tiene que ver con la declaración explícita (o inicialización) de las variables
antes de que se empleen.
Lenguaje multiparadigma
Un lenguaje de programación multiparadigma es aquel que permite la utilización de diferentes
estilos o paradigmas de programación en un mismo programa. Python soporta orientación a
objetos, programación imperativa y, en menor medida, programación funcional. Esto significa
que se pueden usar elementos de programación imperativa, orientada a objetos, funcional, entre
otros, en un solo código.
El Lenguaje Python
A diferencia de la mayoría de los lenguajes de programación, Python nos provee de reglas de
estilos, a fin de poder escribir código fuente más legible y de manera estandarizada. Iremos
viendo a lo largo del resumen estas reglas de estilo, definidas a través de la Python
Enhancement Proposal No 8 (PEP 8: https://www.python.org/dev/peps/pep-0008/).
El estándar PEP8 nos dice cómo formatear el código, pero el "El Zen de Python" (PEP 20:
https://www.python.org/dev/peps/pep-0020/) dice elegantemente: "Lo bello es mejor que lo feo".
PEP 20 es más una filosofía y una mentalidad, de eso se trata la filosofía Pythonic (PEP20):
import this
En estos apuntes haremos uso de la versión 3.8.5 de Python y aunque existen múltiples
implementaciones de Python según el lenguaje de programación que se ha usado para
desarrollarlo, usamos CPython.
!python --version
Python 3.8.5
Elementos del Lenguaje
Como en la mayoría de los lenguajes de programación de alto nivel, Python se compone de una
serie de elementos que definen su estructura. Entre ellos, podremos encontrar los siguientes:
Variables
Una variable es un espacio en la memoria de un ordenador dónde almacenar datos modificables.
En Python, una variable se define con la sintaxis:
nombre_de_la_variable = valor_de_la_variable
Cada variable, tiene un nombre y un valor, el cual define a la vez, el tipo de datos de la variable.
Existe un tipo de “variable”, denominada constante, que se utiliza para definir valores fijos, que
no requieran ser modificados.
PEP8: variables
Utilizar nombres descriptivos y en minúsculas. Para nombres compuestos, separar las
palabras por guiones bajos (snake_case). Antes y después del signo =, debe haber uno (y
solo un) espacio en blanco. Los nombres de variables son sensitivos al uso de
mayúsculas/minúsculas (case-sensitive).
# Correcto
mi_variable = 12
# Incorrecto
MiVariable = 12
mivariable = 12
mi_variable=12
mi_variable = 12
No se debe utilizar las palabras reservadas propias del lenguaje como nombres de variables:
help('keywords')
Here is a list of the Python keywords. Enter any keyword to get more help.
PEP8: constantes
Utilizar nombres descriptivos y en mayúsculas separando palabras por guiones bajos.
# Ejemplo
MI_CONSTANTE = 12
Es fundamental que los nombres de variables sean autoexplicativos, pero siempre llegando a un
compromiso entre ser concisos y claros. Como regla general:
PEP8: identación
Una identación de cuatro espacios en blanco indicará que las instrucciones/expresiones
identadas, forman parte de una misma estructura de control.
Comentarios
Los comentarios son anotaciones que podemos incluir en nuestro programa y que nos permiten
aclarar ciertos aspectos del código. Estas indicaciones son ignoradas por el intérprete de
Python. Los comentarios pueden ser de dos tipos: de una sola línea o multi-línea y se expresan
de la siguiente manera:
PEP8: comentarios
Comentarios en la misma línea del código deben separarse con dos espacios en blanco.
Después del símbolo # debe ir un solo espacio en blanco.
a = 15 # Correcto
a = 15 # Incorrecto
factorial = 4 * 3 * 2 * 1
factorial = 4 * \
3 * \
2 * \
1
factorial = (4 *
3 *
2 *
1)
x = 15
if x > 0:
print('Es positivo')
Es positivo
Para controlar el caso en el que la condición se false (se evalúe como False ) la sentencia if
puede ir seguida por un bloque else :
x = -15
if x > 0:
print('Es positivo')
else:
print('Es negativo')
Es negativo
Las sentencias if - else pueden anidarse para dar respuesta a condiciones más complejas:
x = 0
if x > 0:
if x < 5:
print('Positivo pero menor que 5')
else:
print('Positivo y mayor o igual a 5')
else:
if x == 0:
print('Igual a zero')
else:
print('Es negativo')
Igual a zero
Python nos ofrece una mejora en la escritura de condiciones anidadas cuando aparecen
consecutivamente un else y un if . Podemos sustituirlos por la sentencia elif :
x = 0
if x > 0:
if x < 5:
print('Positivo pero menor que 5')
else:
print('Positivo y mayor o igual a 5')
elif x == 0:
print('Igual a zero')
else:
print('Es negativo')
Igual a zero
Resumiendo, una sentencia if puede ir seguida opcionalmente por uno o más bloques elif
y un bloque final else que sólo se evalúa si todas las condiciones anteriores han sido falsas
(evaluadas a False ). Si alguna de las condiciones previas es cierta (se evalúa a True ) no se
alcanzarán los bloques elif o else posteriores.
x = 4
if x < 0:
print('Es negativo')
elif x == 0:
print('Igual a zero')
elif 0 < x < 5:
print('Positivo pero menor que 5')
else:
print('Positivo y mayor o igual a 5')
Operador ternario
Es una versión del operador if-else en una sola línea. Su sintaxis es:
a = 5
b = 10
max = a if a > b else b
max
10
El número 4 es menor a 5
Sentencia match-case
Disponible a partir de la versión 3.10, puede considerarse una sentencia condicional, en su
versión más simple permite comparar un valor de entrada con una serie de literales sustituyendo
a un conjunto de sentencias if encadenadas.
match variable:
case valor_1:
sentencias
.
.
.
case valor_n:
sentencias
case _:
sentencias
valor = 10
match valor:
case n if n < 0:
print('Negativo')
case 0:
print('Igual a zero')
case n if n < 5:
print('Positivo pero menor que 5')
case _:
print('Positivo y mayor o igual a 5')
Bucle for
Los bucles for permiten recorrer aquellos tipos de datos que sean iterables, es decir, que
admitan iterar/recorrer sobre sus componentes. Algunos ejemplos de tipos y estructuras de
datos que permiten ser iteradas (recorridas) son: cadenas de texto, listas, diccionarios, ficheros,
etc. La sintaxis estándar para un bucle for es:
p-y-t-h-o-n-
Bucle while
Un bucle while especifica una condición y un bloque de código que se ejecutará hasta que la
condición se evalúe como False . La sintaxis estándar para un bucle while es:
while condicion:
<instrucciones> # suele incluir la actualización de la condición
# sumatorio de x o x!
x = 5
total = 0
while x > 0:
total += x
x -= 1
total
15
Python permite combinar el bucle while con una sentencia else como parte del propio
bucle. Si el bucle while finaliza normalmente (no se cumple la condición) el flujo de control
pasa a la sentencia opcional else .
# sumatorio de x o x!
x = 5
total = 0
while x > 0:
total += x
x -= 1
else:
print(total)
15
Los bucles for y while pueden ser interrumpidos con la sentencia break : cuando se
ejecuta, el programa sale del bucle y continúa ejecutando el resto del código.
0
1
2
3
La sentencia continue , cuando se ejecuta, obliga a Python a dejar de ejecutar el código que
haya dentro del bucle y a iniciar una nueva iteración (es decir, a volver al comienzo del bucle y
seguir con la ejecución del programa).
x = 0
while x < 8:
x += 1
if x % 2 == 0: # x es par
continue
print(x)
1
3
5
7
La sentencia pass no hace nada. Puede utilizarse cuando se requiere una declaración
sintácticamente, pero el programa no requiere ninguna acción.
while True:
pass # en espera de la interrupción del teclado (Ctrl+C)
Otro uso habitual es como un marcador de posición para una función o cuerpo condicional
cuando está trabajando en un nuevo código, lo que le permite seguir pensando a un nivel más
abstracto, pero con una sintaxis correcta:
def initlog(*args):
pass # ¡recuerda implementar esto!
Anidamiento de bucles
Tanto el blucle for como el blucle while permiten el anidamiento de otrso bucles o
sentencias condicionales en su cuerpo de instrucciones:
1a 1b 1c 2a 2b 2c 3a 3b 3c
Tipos de datos
Tipos numéricos (int, float y complex)
Hay tres tipos numéricos distintos: enteros, números en coma flotante y números complejos.
Además, los booleanos son un subtipo de enteros. Los enteros tienen una precisión ilimitada.
Los números de punto flotante se implementan usualmente utilizando el double en C. Los
números complejos tienen una parte real e imaginaria, que son cada uno un número de punto
flotante. Para extraer estas partes de un número complejo z , use z.real y z.imag . La
biblioteca estándar incluye tipos numéricos adicionales, fraction que contienen números
racionales y decimal que contienen números en coma flotante con una precisión definible por
el usuario.
En Python no existe límite de tamaño para los números enteros. Tiene un límite modificable de
4300 dígitos a través de la función sys.set_int_max_str_digits() . Para el caso de los
valores en coma flotante si existe un límite de representación:
2.2250738585072014e-308 (sys.float_info.min)
1.7976931348623157e+308 (sys.float_info.max)
Operadores Aritméticos
Entre los operadores aritméticos que Python utiliza, podemos encontrar los siguientes:
Símbolo Significado
+ Suma
- Resta
* Multiplicación
** Exponenciación
/ División
// División entera
Para crear números de un tipo específico, conversión explícita, se pueden utilizar los
constructores int() , bool() , float() y complex() .
Se puede asignar el valor infinito positivo con float(inf) y el infinito negativo con float(-inf).
Prioridad Operador
1 (mayor) ()
2 **
3 -a +a
4 * / // %
5 (menor) + -
Operadores Relacionales
Entre los operadores relacionales que Python utiliza, podemos encontrar los siguientes:
Símbolo Significado
== igual que
!= distinto que
is identidad de objeto
Python ofrece la posibilidad de ver si un valor está entre dos límites de manera directa:
Operadores Lógicos
Entre los operadores lógicos que Python utiliza, podemos encontrar los siguientes:
Símbolo Significado
and Y lógica
or O lógica
xor O exclusiva
not negación
Cortocircuito lógico: Las expresiones lógicas no se evalúan siempre por completo, es decir,
si la evaluación de la parte inicial de la expresión fuerza un resultado, el resto no se evalua.
Símbolo Significado
& Y lógica
| O lógica
^ O exclusiva
~ negación
PEP8: operadores
Siempre colocar un espacio en blanco, antes y después de un operador.
Operación Significado
s+t concatenación de s y t
len(s) longitud de s
Hay que tener en cuenta que el indexado de cualquier secuencia siempre empieza en 0 y
termina en una unidad menos de la longitud de la secuencia.
Operación Significado
Textos/cadenas (str)
Los datos de tipo texto en Python se manejan con objetos str , o cadenas. Las cadenas son
secuencias inmutables de caracteres Unicode. Los literales de tipo cadena de caracteres
pueden estar escritos de varias maneras:
Las cadenas entre comillas triples pueden abarcar varias líneas: todos los espacios en blanco
asociados se incluirán en el literal de la cadena.
La cadena vacía es aquella que no contiene ningún carácter. Aunque a priori no lo pueda parecer,
es un recurso importante en cualquier código.
Out[1]:
Primera línea
Segunda línea
Tercera línea
Si nos interesa que los caracteres especiales pierdan ese significado y poder usarlos de otra
manera se puede utilizar el modo «raw data» que se aplica anteponiendo una r a la cadena de
texto.
Out[1]:
Primera línea\nSegunda línea\nTercera línea
In [1]: a = 'cadena'
• El método split() . Se utiliza para dividir cadenas de texto por algún tipo de separador. El
separador por defecto es el espacio en blanco:
texto = 'a-b-c-d-e-f-g'
texto.split('-')
• El método strip() . Se utiliza para eliminar caracteres del principio y del final de una
cadena. También existen variantes para aplicarla únicamente al comienzo o únicamente al
final de la cadena de texto:
'1234567890'
texto = 'a-b-c-d-e-f-g'
texto.find('b')
• El método count() . Indica el número de veces que aparece una subcadena dentro de la
cadena:
texto = 'a-b-c-d-e-f-g'
texto.find('-')
• El método replace() . Permite sustituir una subcadena por otra dentro de la cadena.
TAmbién permite indicar el número de sustirucxiones a realizar:
texto = 'a-b-c-d-e-f-g'
texto.replace('-', ':')
'a:b:c:d:e:f:g'
texto = 'a-b-c-d-e-f-g'
texto.replace('-', ':', 3)
'a:b:c:d-e-f-g'
La función print()
Es una forma sencilla y rápida de mostrar por pantalla cualquier contenido como una cadena de
texto:
msg1 = 'Hola'
msg2 = 'Mundo'
print(msg1, msg2)
print(msg1, msg2, sep=', ')
print(msg1, msg2, sep='-', end='!!!')
Hola Mundo
Hola, Mundo
Hola-Mundo!!!
Interpolar (en este contexto) significa sustituir una variable por su valor dentro de una cadena de
texto. Consiste en sustituir los nombres de variables por sus valores cuando se construye una
cadena. Para indicar en Python que una cadena es un f-string basta con precederla de una
f e incluir las variables o expresiones a interpolar entre llaves { ... } .
nombre = 'Antonio'
edad = 45
profesion = 'fontanero'
f'Me llamo {nombre}, tengo {edad} años y soy {profesion}'
Los f-strings proporcionan una gran variedad de opciones de formateado: ancho del texto,
número de decimales, tamaño de la cifra, alineación, etc.
valor = 123.456789
f'{valor:10.3f}' # valor:ancho.precision
' 123.457'
f'{valor:10.3f}' # valor:'relleno'ancho.precision
' 123.457'
f'{valor:.010f}' # valor:ancho.'relleno'precision
'123.4567890000'
A partir de Python 3.8, los f-strings permiten imprimir el nombre de la variable y su valor,
como un atajo para depurar nuestro código.
f'{nombre=}'
"nombre='Antonio'"
Comparar cadenas
Cuando comparamos dos cadenas de texto lo hacemos en términos lexicográficos. Es decir, se
van comparando los caracteres de ambas cadenas uno a uno y se va mirando cuál está «antes».
True
La función str está diseñada para devolver representaciones de valores que son bastante
legibles para las personas, mientras que repr está diseñada para generar representaciones
que pueden ser leídas por el intérprete (o forzarán un error de sintaxis si no hay una sintaxis
equivalente). Para los objetos que no tienen una representación particular para el "consumo
humano", str devolverá el mismo valor que repr .
import datetime
hoy = datetime.datetime.now()
print(str(hoy))
print(repr(hoy))
2023-08-25 19:21:47.646199
datetime.datetime(2023, 8, 25, 19, 21, 47, 646199)
str muestra la fecha de hoy de manera que el usuario pueda entender la fecha y la hora,
mientras que repr imprime una representación "oficial" de un objeto de fecha y hora (significa
que usando la representación de cadena "oficial" podemos reconstruir el objeto).
Codificación
Desde Python 3.0, las cadenas se almacenan como Unicode. Unicode es un sistema de
codificación de caracteres utilizado por los equipos informáticos para el almacenamiento y el
intercambio de datos en formato de texto. Asigna un número único (un punto del código) a cada
carácter de los principales sistemas de escritura del mundo. De modo que cada cadena de
caracteres es solo una secuencia de códigos numéricos.
codigo_letra_A = ord('A')
codigo_letra_A
65
letra_A = chr(codigo_letra_A)
letra_A
'A'
# cadena Unicode
cadena = 'pythön!'
print('La cadena es:', cadena)
# codificación en utf_32
print('La versión codificada es:', cadena)
# decodificación en utf_32
print('La versión decodificada es:', cadena.decode('utf_32','strict'))
Tuplas (tuple)
Las tuplas son secuencias inmutables, que normalmente se utilizan para almacenar
colecciones de datos heterogéneos. Las tuplas también se utilizan para casos en los que se
necesita una secuencia inmutable de datos homogéneos. Las tuplas se pueden construir de
varias maneras:
Un iterable es cualquier secuencia, contenedor u objeto que permite iterar por sus
componentes. Es la coma la que forma una tupla, no los paréntesis. Los paréntesis son
opcionales, excepto en el caso de la tupla vacía, o cuando son necesarios para evitar la
ambigüedad sintáctica.
()
una_tupla = 4, 5, 6, 7
una_tupla
(4, 5, 6, 7)
otra_tupla = tuple('cadena')
otra_tupla
'c'
# Una vez creada la tupla, no es posible modificar los objetos que se almacenan en la misma:
otra_tupla[0] = 'x'
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
<ipython-input-42-a97109b690f7> in <module>
1 # Una vez creada la tupla, no es posible modificar los objetos que se almac
enan en la misma:
----> 2 otra_tupla[0] = 'x'
# Se pueden concatenar tuplas utilizando el operador + para producir tuplas más largas
(4, None, 'foo') + (6, 0) + ('bar',)
# Multiplicar una tupla por un número entero, tiene el efecto de concatenar juntas tantas cop
('foo', 'bar') * 4
Desempaquetando tuplas
Python permite asignar elementos de una tupla a variables:
una_tupla = (4, 5, 6)
a, b, c = una_tupla
print(a)
print(b)
print(c)
4
5
6
# Incluso secuencias con tuplas anidadas pueden ser desempaquetadas
una_tupla = 4, 5, (6, 7)
a, b, (c, d) = una_tupla
c
# La asignación múltiple en Python permite intercambiar fácilmente los valores entre variable
a, b = 1, 7
print(f'{a=} y {b=}')
a=1 y b=7
(b, a) = (a, b)
print(f'{a=} y {b=}')
a=7 y b=1
# Un uso común del desempaquetado de variables es iterar sobre secuencias de tuplas o listas:
secuencia = (1,2,3),(4,5,6),(7,8,9)
for a, b, c in secuencia:
print(f'{a=}, {b=}, {c=}')
valores = 1, 2, 3, 4, 5
primer_valor, segundo_valor, *resto_valores = valores
print(f'{primer_valor=}, {segundo_valor=}, {resto_valores=}')
_=[2, 3, 4]
a = (1, 2, 2, 2, 3, 4, 2)
a.count(2)
4
Rangos (range)
El tipo de rango representa una secuencia inmutable de números y se usa comúnmente para
hacer un bucle un número específico de veces o para recorrer componentes iterables. Los
rangos se utilizan por optimizacion de memoria, si bien un rango generado puede ser
arbitrariamente grande, su uso de memoria en un momento dado suele ser muy pequeño.
Los argumentos para el constructor de rangos deben ser enteros. Si se omite el argumento
start el valor predeterminado es 0. Si se omite el argumento de step , el valor
predeterminado es 1. Los rangos soportan índices negativos.
rango = range(10)
rango
range(0, 10)
0 1 2 3 4 5 6 7 8 9
5 4 3 2 1
-1 -2 -3 -4 -5 -6 -7 -8 -9
-15 in rango
False
rango.index(-7)
rango[0:3]
rango[-1]
-9
range(0)
range(0, 0)
Listas (list)
Las listas son secuencias mutables, que generalmente se utilizan para almacenar colecciones
de elementos homogéneos (donde el grado de similitud variará según la aplicación). Las listas
se pueden construir de varias maneras:
lista_vacia = list()
lista_vacia # evaluable como 'False'
[]
[2, 3, 7, None]
una_lista = list(range(10))
una_lista
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
una_lista[1] ='peekaboo'
una_lista
una_lista.append('dwarf')
una_lista
una_lista.insert(3, 'root')
una_lista
['foo', 'peekaboo', 'baz', 'root', 'dwarf']
una_lista.pop(2)
'baz'
una_lista
una_lista.remove('root')
una_lista
'bar' in una_lista
False
True
Dada una lista, podemos convetirla a una cadena de texto, uniendo todos sus elementos
mediante algún separador. Para ello hacemos uso del método join() presente en las cadenas
de texto con la siguiente estructura:
separador.join(lista)
una_lista = list('abcdef')
','.join(una_lista)
'a,b,c,d,e,f'
otra_lista = [7, 2, 5, 1, 3]
otra_lista.sort()
otra_lista
[1, 2, 3, 5, 7]
enumerate
Cuando se realiza una iteración sobre una secuencia es común realizar un seguimiento del índice
del elemento actual. Un enfoque habitual se vería así:
i = 0
for valor in coleccion:
# hacer algo con valor
i += 1
Python tiene una función incorporada, enumerate , que devuelve una secuencia de tuplas (i,
valor):
0 -> foo
1 -> bar
2 -> baz
sorted
La función sorted devuelve una nueva lista ordenada de los elementos de cualquier
secuencia, no modifica la secuencia original:
mi_tupla = 7, 1, 2, 6, 0, 3, 2
sorted(mi_tupla)
[0, 1, 2, 2, 3, 6, 7]
mi_tupla
(7, 1, 2, 6, 0, 3, 2)
sorted('horse race')
[' ', 'a', 'c', 'e', 'e', 'h', 'o', 'r', 'r', 's']
zip
Esta función "empareja" los elementos de una serie de listas, tuplas u otras secuencias para
crear una lista de tuplas:
<zip at 0x7f1fe9ecd800>
(100, foo)
(101, bar)
(102, baz)
Si queremos obtener una lista explícita con la combinación de las listas generadas por zip ,
debemos construir dicha lista de la siguiente manera:
La función zip puede tomar un número arbitrario de secuencias, y el número de elementos que
produce está determinado por la secuencia más corta:
0: (100, foo)
1: (101, bar)
2: (102, baz)
Dada una secuencia "comprimida", zip se puede aplicar de una manera inteligente para
"descomprimir" la secuencia. Otra forma de pensar acerca de esto es convertir una lista de filas
en una lista de columnas:
reversed
reversed(range(10))
<range_iterator at 0x7f1f9901f810>
list(reversed(range(10)))
[9, 8, 7, 6, 5, 4, 3, 2, 1, 0]
Actualmente hay dos tipos de conjuntos incorporados, set y frozenset . El tipo set es
mutable: el contenido se puede cambiar utilizando métodos como add () y remove () .
Dado que es mutable, no tiene valor hash y no se puede utilizar como clave de diccionario ni
como elemento de otro conjunto. El tipo frozenset es inmutable y hashable: su contenido no
puede alterarse después de su creación; por lo tanto, se puede utilizar como una clave de
diccionario o como un elemento de otro conjunto.
Los tipos conjunto admiten operaciones de conjuntos matemáticos como unión, intersección,
diferencia y diferencia simétrica. Las operaciones que suponen actualización de los conjuntos
sólo se pueden aplicar a conjuntos mutables de tipo set . La siguiente tabla recoge las
principales funciones aplicables a conjuntos:
Sintaxis
Función Descripción
alt.
a = {2, 2, 2, 1, 3, 3}
a
{1, 2, 3}
a = set([2, 2, 2, 1, 3, 3])
a
{1, 2, 3}
a = {1, 2, 3, 4, 5}
b = {3, 4, 5, 6, 7, 8}
a.union(b)
{1, 2, 3, 4, 5, 6, 7, 8}
a.intersection(b)
{3, 4, 5}
a.difference(b)
{1, 2}
a.symmetric_difference(b)
{1, 2, 6, 7, 8}
Aunque históricamente Python no establecía que las claves de los diccionarios tuvieran que
mantener su orden de inserción, a partir de Python 3.7 este comportamiento cambió y se
garantizó el orden de inserción de las claves como parte oficial de la especificación del
lenguaje.
Las claves de un diccionario son valores normalmente arbitrarios. los valores que no pueden
tener una valor hash asignado no se pueden usar como claves, es decir, los valores que
contienen listas, diccionarios u otros tipos mutables (que se comparan por valor en lugar de por
identidad de objeto) no pueden ser claves. Los tipos numéricos utilizados como claves obedecen
a las reglas normales de comparación numérica: si dos números se comparan de la misma
manera (como 1 y 1.0), se pueden usar indistintamente para indexar la misma entrada del
diccionario.
{}
Se puede acceder, insertar o establecer elementos utilizando la misma sintaxis que para acceder
a los elementos de una lista o tupla:
un_diccionario['sape']
4139
un_diccionario['b']
---------------------------------------------------------------------------
KeyError Traceback (most recent call last)
<ipython-input-107-32aa0da4e814> in <module>
----> 1 un_diccionario['b']
KeyError: 'b'
Se puede verificar si un diccionario contiene una clave usando la misma sintaxis que se usa para
verificar si una lista o tupla contiene un valor:
'b' in un_diccionario
False
La función get permite recuperar los valores de un diccionario evitando los posibles errores de
acceso por claves inexistentes. Si la clave que buscamos existe, nos devuelve su valor. En caso
de que no exista, nos devuelve None o un valor por valor por defecto si se lo hemos
proporcionado en la llamada:
print(un_diccionario.get('b'))
None
un_diccionario.get('b', 'valor_por_defecto')
'valor_por_defecto'
Se puede eliminar valores utilizando la función del , con el método pop que extrae un
elemento del diccionario por su clave y la borra o con el borrado completo haciendo uso del
método clear :
{'sape': 4139,
'guido': 4127,
'jack': 4098,
'dummy': 'otro valor',
5: 'un valor'}
del un_diccionario['dummy']
un_diccionario
valor = un_diccionario.pop('guido')
valor
4127
un_diccionario
un_diccionario
{}
hash('cadena')
-1179683392021716786
-9209053662355515447
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
<ipython-input-119-a5a61418fc8e> in <module>
----> 1 hash((1, 2, [2, 3])) # falla porque las listas son mutables
# Para usar una lista como clave, una opción es convertirla en una tupla
otro_diccionario = {}
otro_diccionario[tuple([1, 2, 3])] = 5
otro_diccionario
{(1, 2, 3): 5}
mapa = {}
for clave, valor in zip (lista_de_claves, lista_de_valores):
mapa [clave] = valor
Dado que un diccionario es esencialmente una colección de 2-tuplas, la función dict acepta
una lista de 2-tuplas:
list(un_diccionario.keys())
[0, 1, 2, 3, 4]
list(un_diccionario.values())
list(un_diccionario.items())
[(0, 'apple'), (1, 'bat'), (2, 'bar'), (3, 'atom'), (4, 'book')]
un_diccionario.update(otro_diccionario)
un_diccionario
También es posible realizar la operación sin modificar los diccionarios originales haciendo uso
del operador ** :
{**un_diccionario, **otro_diccionario}
A partir de Python 3.9 podemos utilizar el operador | para combinar dos diccionarios:
un_diccionario | otro_diccionario
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
<ipython-input-131-857c5334f21e> in <module>
----> 1 un_diccionario | otro_diccionario
Valores por defecto: setdefault Es muy común seguir la lógica siguiente al trabajar con
diccionarios:
if clave in diccionario:
valor = diccionario[clave]
else:
valor = valor_por_defecto
De este modo, los métodos get y pop pueden tomar un valor predeterminado para ser
devuelto, de modo que el bloque if-else anterior puede escribirse simplemente como:
Es habitual en el uso de diccionarios que los valores sean otras colecciones, como listas. Por
ejemplo, podría imaginar categorizar una lista de palabras por sus primeras letras como un
dictado de listas:
El método setdefault se utiliza para este propósito. El bucle anterior podría escribirse como:
por_letras = {}
for palabra in palabras:
por_letras.setdefault(palabra[0], []).append(palabra)
por_letras
Colecciones avanzadas
Las siguientes colecciones, módulo de collections , son en su mayoría solo extensiones de
colecciones base, algunas de ellas bastante simples y otras un poco más avanzadas. Sin
embargo, para todas ellas es importante conocer las características de las estructuras
subyacentes. Sin comprenderlas, será difícil comprender las características de estas
colecciones. Hay algunas colecciones que se implementan en código C nativo por razones de
rendimiento, pero todas ellas también se pueden implementar fácilmente en Python puro.
0
2
Los objetos Counter admiten tres métodos más allá de los disponibles para todos los
diccionarios:
• elements(), que devuelve un iterador sobre los elementos que se repiten tantas veces como
su conteo.
• most_common([n]), que devuelve una lista de los n elementos mas comunes y sus
conteos, del mas común al menos común.
Counter('abracadabra').most_common(3)
Los objetos deque admiten, entre otros, los siguientes métodos: append(x) ,
appendleft(x) , clear() , copy() , count(x) , extend(iterable) ,
extendleft(iterable) , index(x[, start[, stop]]) , insert(i, x) , pop() ,
popleft() , remove(value) , reverse() y rotate(n=1) . También proporcionan un
atributo de solo lectura maxlen que informa de su tamaño máximo o None si no está limitado.
Cuando se encuentra una clave por primera vez, al no estar en el mapping se crea
automáticamente usando la función list que retorna una list vacía. La operación
list.append() luego adjunta el valor a la nueva lista. Cuando se encuentra una clave
existente, la búsqueda procede normalmente (retornando la lista para esa clave) y la operación
list.append() agrega otro valor a la lista. Esta técnica es más simple y rápida que una
técnica equivalente usando dict.setdefault() .
print(Color.RED)
print(Color(1))
print(Color.RED.name)
print(Color.RED.value)
Color.RED
Color.RED
Color.RED
RED
1
<Color.RED: 1>
Para crear un heapq , use una lista inicializada en [] , o puede transformar una lista poblada
en un heapq a través de la función heapify() . El API contine múltiples funciones para
trabajar con las listas ordenadas.
Como es el caso de heapq , bisect no crea realmente una estructura de datos especial.
Simplemente funciona en una lista estándar y espera que esa lista siempre esté ordenada. Es
importante comprender las implicaciones de rendimiento de esto; simplemente agregar
elementos a la lista usando el algoritmo bisect puede ser muy lento porque una inserción en una
lista toma O (n) . Efectivamente, crear una lista ordenada usando bisect requiere O (n *
n) , que es bastante lento, especialmente porque crear la misma lista ordenada usando heapq
o sorted toma O (n * log (n)) en su lugar.
[1, 2, 3, 5]
# Using bisect:
import bisect
sorted_list = []
bisect.insort(sorted_list, 5) # O(n) = O(1)
bisect.insort(sorted_list, 3) # O(n) = O(2)
bisect.insort(sorted_list, 1) # O(n) = O(3)
bisect.insort(sorted_list, 2) # O(n) = O(4)
sorted_list
[1, 2, 3, 5]
Para una pequeña cantidad de elementos, la diferencia es insignificante, pero crece rápidamente
hasta un punto en el que la diferencia será grande. Sin embargo, la búsqueda dentro de la lista
es muy rápida; debido a que está ordenado, podemos usar un algoritmo de búsqueda binaria
muy simple, de forma que nunca se necesitarán más de O(log(n)) pasos para encontrar un
número.
Tipado dinámico
Una de las características de las variables en Python es que pueden tomar valores de distinto
tipo a lo largo del código, propiedad que se conoce como tipado dinámico. Este tipo de
flexibilidad es una pieza que hace que Python y otros lenguajes similares sean fáciles de usar.
Comprender cómo funciona el tipado dinámico es una pieza importante de aprendizaje para
analizar datos de manera eficiente y efectiva con Python. Pero lo que este tipo de flexibilidad
también apunta es el hecho de que las variables de Python son más que solo su valor; también
contienen información adicional sobre el tipo de valor.
Siempre es posible recuperar el tipo de una variable haciendo uso de la función type.
variable = 3
type(variable)
int
variable = 3.7
type(variable)
float
variable = "3"
type(variable)
str
variable = True
type(variable)
bool
variable = (3, 5, 6)
type(variable)
tuple
variable = [3, 5, 6]
type(variable)
list
variable = {3, 5, 6}
type(variable)
set
dict
La implementación estándar de Python está escrita en C (CPython). Esto significa que cada
objeto de Python es simplemente una estructura en C inteligentemente disfrazada, que contiene
no solo su valor, sino también otra información. Mirando a través del código fuente de Python
3.4, encontramos que la definición de tipo entero (largo) efectivamente se ve así (una vez que se
expanden las macros C):
struct _longobject {
long ob_refcnt; # recuento de referencias para manejo de
memoria
PyTypeObject *ob_type; # tipo de variable
size_t ob_size; # tamaño de los datos
long ob_digit[1]; # value entero actual
};
Un entero C es esencialmente una etiqueta a una posición en la memoria cuyos bytes codifican
un valor entero. Un entero en Python es un puntero a una posición en la memoria que contiene
toda la información del objeto de Python, incluidos los bytes que contienen el valor entero.
Esta información adicional, que permite que Python se codifique de manera tan libre y dinámica,
tiene un costo, que se vuelve especialmente evidente en estructuras que combinan muchos de
estos objetos. Por ejemplo, el contenedor multielemento mutable estándar en Python es la lista,
y como hemos visto aunque generalmente se utiliza para datos homogéneos, también se pueden
crear listas heterogéneas:
Pero esta flexibilidad tiene un costo: para permitir estos tipos flexibles, cada elemento de la lista
debe contener su propia información de tipo, recuento de referencias y otra información, es
decir, cada elemento es un objeto Python completo. En el caso especial de que todas las
variables sean del mismo tipo, gran parte de esta información es redundante: puede ser mucho
más eficiente almacenar datos en una matriz de tipo fijo.
Asignación en Python
La asignación en Python, por defecto, manipula referencias de objetos. Es decir, las variables en
Python no guardan directamente valores ni objetos sino referencias a éstos. Por lo que cuando
se hace una asignación no se están copiando esos valores:
Estos mecanismos del intérprete para referenciar variables y duplicar la información se utilizan
junto a otros para optimizar el uso de los recursos. Es una característica del lenguaje es muy útil,
pero es necesario tenerla en cuenta siempre. Además, su comportamiento depende de la
mutabilidad de los datos que intervengan en la asignación:
# comportamiento idéntico
# a = 100
# b = 100
a = 100
b = a
a = 100
b = 100
id(a) = 94892117678432
id(b) = 94892117678432
a = 0
b = 100
id(a) = 94892117675232
id(b) = 94892117678432
a = [1, 2, 3]
b = a
a = [1, 2, 3]
b = [1, 2, 3]
id(a) = 139773700955968
id(b) = 139773700955968
a.append(4)
a = [1, 2, 3, 4]
b = [1, 2, 3, 4]
id(a) = 139773700955968
id(b) = 139773700955968
b[2][0] = 5
Un caso con especial es el corte de las listas que no genera copias de los objetos en la lista; sólo
copia las referencias a ellos. Es lo que se denomina una copia superficial. Cuando la lista está
compuesta por elementos inmutables no es ningún problema, los cambios en la lista original no
afectan a la sección puesto que son variables distintas, pero cuando la lista está compuesta por
elementos mutables hay que tener en cuenta que éstos comparten la referencia:
a[0] = 0
a[2][1] = 5
# a y b comparten las referencias a los objetos, pero no dirección/id
print("a =",a)
print("b =",b)
print("id(a) =",id(a))
print("id(b) =",id(b))
La diferencia entre copia superficial y profunda solo es relevante para objetos compuestos
(objetos que contienen otros objetos, como listas o instancias de clase):
• Una copia superficial (shallow copy) construye un nuevo objeto compuesto y luego (en la
medida de lo posible) inserta referencias en él a los objetos encontrados en el original.
• Una copia profunda (deep copy) construye un nuevo objeto compuesto y luego,
recursivamente, inserta copias en él de los objetos encontrados en el original.
Por ello, un objeto copiado de forma superficial no es completamente independiente del original
como sí ocurre con uno obtenido de una copia profunda, implicando esta última un proceso de
creación más lento.
Para copias superficiales además de la función copy se pueden utilizar la funciones list ,
dict y set empleadas para declarar listas, diccionarios y conjuntos, respectivamente.
import copy
a = [1, 2, [3, 4]]
b = copy.deepcopy(a) # copia profunda
a[0] = 0
a[2][1] = 5
# a y b no comparten nada
print("a =",a)
print("b =",b)
print("id(a) =",id(a))
print("id(b) =",id(b))
En la siguiente tabla
podemos observar el comportamiento asintótico de los diferentes órdenes de complejidad:
Python cuenta con una implementación en gran parte de su código que busca tanto la eficiencia
del almacenamiento (asignación) como la eficiencia del tiempo (compresiones).
resultado = []
for valor in coleccion:
if condicion (valor):
resultado.append(expresion(valor))
La condición de filtrado puede omitirse en caso de no ser necesaria.
Para un conjunto sólo habría que sustituir las llaves por corchetes:
{1, 2, 3, 4, 6}
En general, las operaciones sobre compresiones serán uno o dos (o más) órdenes de magnitud
más rápidas que sus equivalentes puras de Python, con el mayor impacto en cualquier tipo de
cálculo numérico, ver temporización y profiling:
%%time
a = range(100000000)
b = []
for i in a:
b.append(i^2)
Comprensiones anidadas
# Queremos obtener una lista única que contenga todos los nombres con una o más letras 'e' en
datos = [['John', 'Emily', 'Michael', 'Mary', 'Steven'],
['Maria', 'Juan', 'Javier', 'Natalia', 'Pilar']]
nombres_buscados = []
for lista_nombres in datos:
for nombre in lista_nombres:
if nombre.upper().count('E') >= 1:
nombres_buscados.append(nombre)
nombres_buscados
[1, 2, 3, 4, 5, 6, 7, 8, 9]
Funciones
Las funciones son el método principal y más importante de organización de código y
reutilización en Python. Son el mecanismo básico para conseguir la descomposición y la
abstracción del problema a resolver. Como regla general, si se anticipa la necesidad de repetir el
mismo código o uno muy similar más de una vez, puede valer la pena escribir una función
reutilizable. Las funciones también pueden ayudar a hacer que el código sea más legible al darle
un nombre a un grupo de declaraciones. Por último, mantienen el código organizado y
coherente.
Las funciones se declaran con la palabra clave def y devuelven los resultados de su operación
(en forma de literales, variables y expresiones) con la palabra clave return . La sintaxis básica
sería:
def nombre_funcion([paramétros]):
<instrucciones>
[return resultado]
La cláusula return es opcional, en caso de alcanzar el final de una función sin encontrar una
declaración de retorno, se devuelve automáticamente None (representa la ausencia de valor).
Por otra parte, no hay problema con tener múltiples declaraciones return . Una cláusula
return puede devolver cualquier tipo de objeto, una lista, una tupla, un diccionario, otra
función, etc.
No confundir return con print. El valor de retorno de una función nos permite usarlo fuera de
su contexto, mientras que imprimir un valor o expresión sólo permite su visualización. Una
función que finaliza su cuerpo con una llamada a print devuelve con resultado de su
ejecución None.
Si una función no dispusiera de valores de entrada estaría muy limitada en su actuación. Es por
ello que los parámetros nos permiten variar los datos que consume una función para obtener
distintos resultados.
# definición de la función
def sumatorio(x, y):
print(f'{x=}, {y=}', end=' -> ')
return x + y
Cuando llamamos a una función con argumentos, los valores de estos argumentos se copian
en los correspondientes parámetros dentro de la función
resultado = sumatorio(3, 5)
print(resultado)
Cada función puede tener parámetros posicionales y parámetros de tipo palabra clave. Los
parámetros de tipo palabra clave se usan comúnmente para especificar valores predeterminados
o parámetros opcionales:
PEP8: Funciones
A la definición de una función la deben anteceder dos líneas en blanco. Al asignar parámetros
por defecto, no debe dejarse espacios en blanco ni antes ni después del signo '='.
resultado = sumatorio_ponderado(3, 5)
print(resultado)
resultado = sumatorio_ponderado(3, 5, 0.5)
print(resultado)
resultado = sumatorio_ponderado(3, 5, z=2)
print(resultado)
La principal restricción al pasar argumentos a una función es que los de tipo palabra clave deben
seguir a los argumentos posicionales (si los hay). Un argumento posicional nunca puede pasarse
después de una agurmento de tipo palabra clave:
Si al pasar los argumentos hacemos uso de los nombres de los parámetros, éstos se pueden
pasar en cualquier orden:
resultado = sumatorio_ponderado(x=3, y=8, z=2)
print(resultado)
resultado = sumatorio_ponderado(y=8, z=2, x=3)
print(resultado)
Es importante tener presente que los valores por defecto en los parámetros se fijan cuando
se define la función, no cuando se ejecuta:
MULTIPLICADOR = 1
resultado = sumatorio_ponderado(3, 5)
print(resultado)
MULTIPLICADOR = 2
resultado = sumatorio_ponderado(3, 5)
print(resultado)
Al igual que en otros lenguajes de alto nivel, es posible que una función, espere recibir un
número arbitrario -desconocido- de parámetros. Estos parámetros llegarán a la función en forma
de tupla de argumentos. Para definir un número arbitrarios de parámetros posicionales en una
función, se antecede al parámetro un asterisco ( * ).
Es posible también, definir un conjunto arbitrarios parámetros de tipo palabra clave como pares
de clave=valor . En estos casos, al nombre del parámetro deben precederlo dos astericos
( ** ). Estos parámetros llegarán a la función en forma de diccionario:
resultado = sumatorio_ponderado(3, 5)
print(resultado)
resultado = sumatorio_ponderado(3, 5, z=2)
print(resultado)
resultado = sumatorio_ponderado(3, 5, 0.5)
print(resultado)
Si mezclamos las dos estrategias anteriores podemos forzar a que una función reciba
argumentos de un modo concreto.
def cuadrado(x):
return x*x
def doble(x):
return 2*x
doble(cuadrado(3))
18
Desde el cuerpo de una función se puede invocar a una función pasada como argumento:
doble(1) -> 2
doble(2) -> 4
doble(3) -> 6
doble(4) -> 8
cuadrado(1) -> 1
cuadrado(2) -> 4
cuadrado(3) -> 9
cuadrado(4) -> 16
def cuadrados(*valores):
cuadrados = []
for valor in valores:
cuadrados.append(valor*valor)
return cuadrados
cuadrados (1, 2, 3, 4)
[1, 4, 9, 16]
Documentando funciones
De forma similar a los comentarios que permiten explicar partes del código desarrollado, es
posible comentar las funciones. Para ello se debe incluir una cadena de texto, denominada
docstring , antes del inicio del cuerpo de la función. La forma más estándar es haciendo uso
de comilla triples:
def cuadrados(*valores):
''' La función devuelve una lista con el cuadrado de
cada valor proporcionado en el parámetro 'valores'
'''
cuadrados = []
for valor in valores:
cuadrados.append(valor*valor)
return cuadrados
help(cuadrados)
cuadrados(*valores)
La función devuelve una lista con el cuadrado de
cada valor proporcionado en el parámetro 'valores'
Existen diferentes formas de documentar una función, el formato recomendado por Python es
reStructuredText docstrings o RST. Aunque la estructura de los diferentes formatos es similar:
def cuadrados(*valores):
''' Obtiene los cuadrados de los números pasados como argumento
:param valores: valores de los que se obtendran sus cuadrados
:type valores: tuple
:return: valor*valor
:rtype: list
'''
cuadrados = []
for valor in valores:
cuadrados.append(valor*valor)
return cuadrados
Anotación de tipos
Las anotaciones de tipos se introdujeron en Python 3.5 y permiten indicar tipos para los
parámetros de una función y/o para su valor de retorno (aunque también funcionan en creación
de variables). Las anotaciones de tipos son una herramienta muy potente y que, usada de forma
adecuada, permite complementar la documentación de nuestro código.
Como se puede observar, vamos añadiendo los tipos después de cada parámetro utilizando :
como separador. En el caso del valor de retorno usamos la flecha -> .
Las variables definidas dentro de una función tienen alcance local. Su alcance es limitado dentro
de la función. Considere la siguiente función:
def funcion():
a = []
for i in range(5):
a.append(i)
Cuando se llama a funcion() , se crea la lista vacía a , se agregan cinco elementos y luego se
destruye a cuando se sale de la función. En este caso después de la ejecución de
funcion() , la variable a no estaría definida.
Cuando existen dos variables con el mismo nombre dentro y fuera de la función, se tratan como
variables diferentes. Supongamos que en cambio hubiéramos declarado lo siguiente:
a = [1, 2, 3]
def funcion():
a = []
for i in range(4, 7):
a.append(i)
La asignación de variables fuera del alcance de la función es posible, pero esas variables deben
declararse como globales a través de la palabra clave global :
a = [1, 2, 3]
def funcion():
global a
for i in range(4, 7):
a.append(i)
El uso de global no se considera una buena práctica ya que puede inducir a confusión y tener
efectos colaterales indeseados.
Al asignar una variable a otras distintas estaremos obteniendo alias del mismo objeto. Si la
variable de la que partimos es mutable, cualquier modificación en una alias supone un cambio
en en todas las variables. La frase clave a tener en cuenta cuando se trabaja con variables
mutables es efectos secundarios.
Es muy importante tener en cuenta los efectos secundarios que se pueden dar cuando se
realizan operaciones mutables durante iteraciones:
l1 = [1, 2, 3, 4]
l2 = [1, 2, 5, 6]
eliminar_duplicados_erronea(l1, l2)
print(f'{l1=}')
l1=[2, 3, 4]
El resultado es [2, 3, 4] y no [3, 4] como se podría esperar. Esto se debe a que Python
usa un contador interno para realizar el seguimiento del índice de recorrido del bucle y la
mutación cambia la longitud de la lista pero Python no actualiza el contador. Como consecuencia
el bucle nunca ve el elemento 2 .
Para una solución correcta es necesario duplicar la lista sobre la que se eliminan los elementos:
eliminar_duplicados(l1, l2)
print(f'{l1=}')
l1=[3, 4]
Estos efectos secundarios también pueden dase cuando utilizamos parámetros por defecto que
utilizan objetos mutables:
apilar_elementos_erronea(1, [])
apilar_elementos_erronea(2, [])
apilar_elementos_erronea(4, [1, 2, 3])
resultado=[1]
resultado=[2]
resultado=[1, 2, 3, 4]
apilar_elementos_erronea(5)
apilar_elementos_erronea(6)
resultado=[5]
resultado=[5, 6]
apilar_elementos(1, [])
apilar_elementos(2, [])
apilar_elementos(4, [1, 2, 3])
apilar_elementos(5)
apilar_elementos(6)
resultado=[1]
resultado=[2]
resultado=[1, 2, 3, 4]
resultado=[5]
resultado=[6]
def doblar(x):
return x * 2
lambda x: x * 2
Una función lambda tiene las siguientes propiedades:
1. Se escribe en una única sentencia (línea).
2. No tiene nombre (anónima).
3. Su cuerpo conlleva un return implícito.
4. Puede recibir cualquier número de parámetros.
Son especialmente convenientes en el análisis de datos porque, como verermos, hay muchos
casos en los que las funciones de transformación de datos tomarán funciones como
argumentos. A menudo lleva menos codificación (y más claridad) pasar una función lambda en
lugar de escribir la declaración completa de una función o incluso asignar la función lambda a
una variable local.
La función map permite aplicar una función a cada uno de los elementos que componen un
iterable:
map(funcion, iterable)
<map at 0x7f1db7bc9af0>
La función map no devuelve una lista con el resultado de aplicar la función al iterable, sino un
generador sobre el que podemos iterar para recuperar los resultados.
La función filter selecciona aquellos elementos de un iterable que cumplan una determinada
condición.:
filter(funcion, iterable)
<filter at 0x7f1db7bc9310>
Al igual que en el caso anterior, la función filter no devuelve una lista con el resultado filtrar
los elementos del iterable, sino un generador sobre el que podemos iterar para recuperar los
resultados.
[2, 4, 6, 8]
De nuevo podemos obtener el mismo resultado haciendo uso de una lista por compresión:
[2, 4, 6, 8]
Por cuestiones de legibilidad del código, se suelen preferir las listas por comprensión a
funciones como map o filter .
Para utilizar la función reduce debemos utilizar el módulo functools . Nos permite aplicar
una función dada sobre todos los elementos de un iterable de manera acumulativa. Es decir, nos
permite reducir una función sobre un conjunto de valores.
120
cadenas = ['En', 'un', 'lugar', 'de', 'la', 'Mancha', 'de', 'cuyo', 'nombre', '...'
reduce(lambda x, y: x + ' ' + y, cadenas)
def una_funcion(nombre):
return "Hola "+nombre
def otra_funcion(funcion_param):
"""Llamada de retorno a nivel global"""
return globals()[funcion_param]("Laura")
print(otra_funcion("una_funcion"))
Hola Laura
print(locals()["una_funcion"]("Facundo"))
Hola Facundo
Python proporciona funcionalidades para comprobar que una función existe y pueda ser llamada.
El operador in , nos permitirá conocer si un elemento se encuentra dentro de una colección,
mientras que la función callable() nos dejará saber si esa función puede ser llamada.
def otra_funcion(funcion_param):
if funcion_param in globals():
if callable(globals()[funcion_param]):
return globals()[funcion_param]("Laura")
else:
return "Función no encontrada"
print(otra_funcion("pepe"))
Función no encontrada
Iteradores y Generadores
Tener una forma consistente de iterar sobre secuencias, para obtener los objetos de una tupla,
de una lista o incluso las líneas en un archivo, es una característica importante de Python. Para
llevar a cabo esta funcionalidad Python define los iteradores.
def __iter__(self):
return self
def __next__(self):
if self.start >= self.stop:
raise StopIteration
current = self.start * self.start
self.start += 1
return current
def current(self):
return self.start
Para facilitar la creación de iteradores Python los genera de forma implícita cuando hacemos uso
de un blucle for sobre cualquier iterable para obtener cada uno de sus elementos. También
proporciona las funciones iter y next que permiten definir y manejar iteradores de forma
senccilla:
<dict_keyiterator at 0x7f1fe9ee69a0>
next(iterator_un_diccionario)
'a'
next(iterator_un_diccionario)
'b'
next(iterator_un_diccionario)
'c'
next(iterator_un_diccionario)
---------------------------------------------------------------------------
StopIteration Traceback (most recent call last)
<ipython-input-243-05f7bb6cf7af> in <module>
----> 1 next(iterator_un_diccionario)
StopIteration:
Un detalle muy importante es que los generadores «se agotan». Es decir, una vez que ya
hemos consumido todos sus elementos, no obtendremos nuevos valores.
Es posible definir múltiples iteradores basados en el mismo objeto iterable. Cada iterador
mantendrá su propio estado de progreso. Por lo tanto, al definir múltiples instancias de iterador
de un objeto iterable, es posible iterar hasta el final de una instancia mientras la otra instancia
permanece al principio.
Por lo tanto, podemos decir que los iteradores tienen una naturaleza perezosa: cuando se crea
un iterador, los elementos no se obtienen hasta que se solicitan. En otras palabras, los
elementos de nuestra instancia de lista solo se devolverán una vez que les pidamos
explícitamente con next .
Sin embargo, todos los valores de un iterador se pueden extraer a la vez llamando a un
contenedor de estructura de datos iterable integrado (es decir, list , set , tuple ) en el
objeto iterador para forzar al iterador a generar todos los valores. sus elementos a la vez.
list(iter(un_diccionario))
La alternativa más conveniente para implementar un iterador es usar un generador. Aunque los
generadores pueden parecer funciones ordinarias de Python, son diferentes. Para empezar, un
objeto generador no devuelve elementos. En su lugar, utiliza la palabra clave yield para
generar elementos bajo petición. Por lo tanto, podemos decir que un generador es un tipo
especial de función que aprovecha la evaluación perezosa.
def divisores(n):
lista_divisores = []
for valor in range(1, n+1):
if n % valor == 0:
lista_divisores.append(valor)
return lista_divisores
print(divisores(30))
El código anterior devuelve la lista completa de factores. Sin embargo, observe la diferencia
cuando se utiliza un generador en lugar de una función tradicional de Python:
def divisores(n):
for valor in range(1, n+1):
if n % valor == 0:
yield valor
print(divisores(30))
Dado que usamos la palabra clave yield en lugar de return , no se sale de la función
después de la ejecución. De este modo Python creará un objeto generador en lugar de una
función tradicional. Una vez que se ejecuta la palabra clave yield , se suspende la ejecución
de la función. Cuando esto ocurre, se guarda el estado de la función. Por lo tanto, es posible
reanudar la ejecución de la función a nuestra voluntad.
En consecuencia, es posible llamar a la función next en el iterador perezoso para mostrar los
elementos de la serie uno cada vez.
divisores_30 = divisores(30)
print(next(divisores_30))
print(next(divisores_30))
print(next(divisores_30))
for x in divisores_30:
print(x, end=" ")
1
2
3
5 6 10 15 30
Otra forma aún más concisa de hacer un generador es utilizar una expresión de un generador,
similar a las expresiones de compresión de listas y diccionarios, pero utilizamos paréntesis en
vez de corchetes o llaves. Las expresiones generadoras admiten condiciones y anidamiento de
bucles, tal y como se vio con las listas por comprensión:
1
4
9 16 25 36 49 64 81 100
Sin embargo, es importante tener en cuenta sus ventajas y desventajas. Los siguientes son los
pros más importantes:
• Uso de memoria. Los elementos se pueden procesar de uno en uno, por lo que
generalmente no es necesario mantener la lista completa en la memoria.
• Los resultados pueden depender de factores externos, en lugar de tener una lista estática.
Piense en procesar una cola / pila, por ejemplo.
• Los generadores son vagos. Esto significa que si está utilizando solo los primeros cinco
resultados de un generador, el resto ni siquiera se calculará.
• Generalmente, es más sencillo escribir que las funciones generadoras de listas.
• Los resultados están disponibles solo una vez. Después de procesar los resultados de un
generador, no se puede volver a utilizar.
• Se desconoce el tamaño hasta que termine el procesamiento, lo que puede ser perjudicial
para ciertos algoritmos.
• Los generadores no son indexables, lo que significa que generador[5] no funcionará.
Los generadores también permiten que se les envíe información, de forma que podemos
interactuar entre la información enviada y el valor generado en el paso:
def potencias_impares(n=10):
for i in range(1, n + 1, 2):
valor = yield
print(f'-> El generador recibe {valor}')
yield valor ** i
print(f'\tEl generador devuelve {valor}^{i}: {valor ** i}')
generador_potencias_impares = potencias_impares()
next(generador_potencias_impares)
generador_potencias_impares.send(4)
next(generador_potencias_impares)
generador_potencias_impares.send(3)
next(generador_potencias_impares)
generador_potencias_impares.send(2)
next(generador_potencias_impares)
True
validar_email('[email protected]')
False
Un cierre o clausura es una función anidada que nos permite acceder a las variables de la
función externa incluso después de que la función externa esté ejecutada. Los cierres se pueden
usar para evitar valores globales y ocultar datos, y pueden ser una solución elegante para casos
simples con uno o varios métodos. Sin embargo, para casos complejos la implementación de
clase puede ser más apropiada.
def multiplicador_de(n):
def multiplicador(x):
return x * n
return multiplicador
# multiplicador de 3
m3 = multiplicador_de(3)
# multiplicador de
m5 = multiplicador_de(5)
print(m3(9))
print(m5(3))
print(m5(m3(2)))
print(multiplicador_de(3)(7))
27
15
30
21
En una clausura o cierre se devuelve una función, no una llamada a una función.
Decoradores
Un decorador es una función que recibe como parámetro una función y devuelve otra función.
Se podría ver como un caso particular de clausura.
Los decoradores permiten modificar el comportamiento de una función y suelen ser usados para
añadir funcionalidades a una función, como logs, temporización o autentificación, etc.
def log_info(func):
def wrapper(*args, **kwargs):
result = func(*args, **kwargs)
print(f'{func.__name__}{args} devuelve {result}')
return result
return wrapper
sumar_log = log_info(sumar)
sumar_log(5,7)
sumar(5, 7) devuelve 12
12
Python nos ofrece una forma para simplificar la aplicación de los decoradores a través del
operador @ justo antes de la definición de la función que queremos decorar:
@log_info
def sumar(x, y):
return x + y
sumar(5,7)
sumar(5, 7) devuelve 12
12
def assert_int(func):
def wrapper(*args, **kwargs):
if all(isinstance(a, int) for a in args) and \
all(isinstance(kw, int) for kw in kwargs.values()):
return func(*args, **kwargs)
return None
return wrapper
@assert_int
def sumar(x, y):
return x + y
print(sumar(5, 3))
print(sumar(9, 4.5))
8
None
Podemos aplicar más de un decorador a una función. Cuando tenemos varios decoradores se
aplican desde dentro hacia fuera ya que la ejecución de un decorador depende de otro
decorador.
@assert_int
@log_info
def sumar(x, y):
return x + y
sumar(5, 3)
sumar(8.9, 4.5)
sumar(5, 3) devuelve 8
Podemos definir decoradores que admitan parámetros. Para ello definimos una función de
clausura o cierre en función del parámetro. No definimos un decorador sino una factoría de
decoradores:
def assert_type(atype):
def decorator(func):
def wrapper(*args, **kwargs):
if all(isinstance(a, atype) for a in args) and \
all(isinstance(a, atype) for a in kwargs.values()):
return func(*args, **kwargs)
return None
return wrapper
return decorator
@assert_type(float)
def sumar(x, y):
return x + y
print(sumar(5, 3))
print(sumar(9.3, 4.5))
None
13.8
Funciones recursivas
Se trata de funciones que se llaman a sí mismas durante su propia ejecución. Funcionan de
forma similar a las iteraciones, pero debemos encargarnos de planificar el momento en que
dejan de llamarse a sí mismas o tendremos una función rescursiva infinita.
Suele utilizarse para dividir una tarea en subtareas más simples de forma que sea más fácil
abordar el problema y solucionarlo.
@log_info
def factorial(num):
if num > 1:
num = num * factorial(num - 1)
return num
factorial(5)
factorial(1,) devuelve 1
factorial(2,) devuelve 2
factorial(3,) devuelve 6
factorial(4,) devuelve 24
factorial(5,) devuelve 120
120
def fibonacci(n):
if 0 <= n <= 1:
return n
return fibonacci(n - 1) + fibonacci(n - 2)
fibonacci(9)
34
Existe un número máximo de llamadas recursivas que se pueden realizar, para evitar
consumir todos los recursos del sistema.
Los módulos se pueden importar y usar en otros archivos Python usando la declaración de
importación import . Algunos módulos populares en Python son math , random , csv , y
datetime .
Importación de módulos
Ruta de búsqueda de módulos
Cuando el intérprete ejecuta cualquier declaración de importación, busca mod.py en una lista de
directorios recopilados de las siguientes fuentes:
import sys
sys.path
['/workspace/Curso Python',
'/opt/conda/lib/python38.zip',
'/opt/conda/lib/python3.8',
'/opt/conda/lib/python3.8/lib-dynload',
'',
'/root/.local/lib/python3.8/site-packages',
'/opt/conda/lib/python3.8/site-packages',
'/opt/conda/lib/python3.8/site-packages/IPython/extensions',
'/root/.ipython']
La cadena vacía '' en la lista de sys.path hace referencia a la carpeta actual de trabajo.
Por lo tanto, para asegurarse de que se encuentre el módulo a importar, se debe realizar una de
las siguientes acciones:
Una vez que se ha importado un módulo, se puede determinar la ubicación donde se encontró
mediante el atributo __file__ del módulo:
import re
re.__file__
'/opt/conda/lib/python3.8/re.py'
Modos de importación
Supongamos la siguiente estructura de directorios que configura el paquete sound y que está
compuesta por los subpaquetes formats , effects , y filters :
import <module_name>
import sound.effects.echo
Esto carga el submódulo sound.effects.echo . Las funciones o componentes de este
módulo deben ser referenciadas con su nombre completo. Supongamos que existe una función
echofilter :
El contenido del módulo se puede importar desde una definición de función. En ese caso, la
importación no se produce hasta que se llama a la función. Sin embargo, Python 3 no permite
la sintaxis import * indiscriminada desde dentro de una función.
Es posible también abreviar los espacios de nombres mediante un “alias”. Para ello, durante la
importación, se asigna la palabra clave as seguida del alias con el cuál nos referiremos en el
futuro a ese namespace importado:
PEP8: importación
La importación de módulos debe realizarse al comienzo del documento, en orden alfabético
de paquetes y módulos. Primero deben importarse los módulos propios de Python. Luego, los
módulos de terceros y finalmente, los módulos propios de la aplicación. Entre cada bloque de
importaciones, debe dejarse una línea en blanco.
Inicialización de paquetes
Tal y como se ha indicado anterioremente para que un conjunto de módulos en un directorio
pueda ser considerado un paquete o un subpaquete es necesario que contenga un fichero
__init__.py que incluso puede estar vacio.
Este fichero se invoca cuando se importa el paquete o un módulo del paquete y se puede utilizar
para la ejecución del código de inicialización del paquete, como la inicialización de datos a nivel
de paquete. Se pueden definir estructuras de datos, variables o constantes como variables
globales a nivel de paquete que pueden ser importadas por los diferentes módulos. También se
puede utilizar para efectuar la importación automática de módulos desde un paquete.
Una de las funcionalidades más útiles del fichero __init__.py es el control de la carga de
módulos de un paquete cuando se hace uso de la sintaxis de importación import * . Como
hemos visto cuando se utiliza esta sintaxis para un módulo, todos los objetos del módulo se
importan a la tabla de símbolos local, excepto aquellos cuyos nombres comienzan con un guión
bajo. Sin embargo cuando se trata de un paquete Python sigue esta convención: si existe el
archivo __init__.py en el directorio del paquete y contiene una lista llamada __all__ , esta
se considera una lista de módulos que deben importarse cuando se encuentra la declaración de
<package_name> import * . Si no existe esta variable no se importa nada.
Por cierto, __all__ también se puede definir en un módulo y tiene el mismo propósito,
controlar lo que se importa con import * .
A partir de Python 3.3, con la introducción de espacios de nombres implícitos para los
paquetes, se permite la creación de un paquete sin ningún archivo __init__.py. Por supuesto,
todavía puede estar presente si es necesaria la inicialización del paquete.
La función dir
La función incorporada dir devuelve una lista de nombres definidos en un espacio de
nombres. Sin argumentos, produce una lista de nombres ordenados alfabéticamente en la tabla
de símbolos local actual. Esto puede resultar útil para identificar qué se ha agregado
exactamente al espacio de nombres mediante una declaración de importación.
Cuando se le da un argumento que es el nombre de un módulo, dir enumera los nombres
definidos en el módulo:
import math
dir(math)
['__doc__',
'__file__',
'__loader__',
'__name__',
'__package__',
'__spec__',
'acos',
'acosh',
'asin',
'asinh',
'atan',
'atan2',
'atanh',
'ceil',
'comb',
'copysign',
'cos',
'cosh',
'degrees',
'dist',
'e',
'erf',
'erfc',
'exp',
'expm1',
'fabs',
'factorial',
'floor',
'fmod',
'frexp',
'fsum',
'gamma',
'gcd',
'hypot',
'inf',
'isclose',
'isfinite',
'isinf',
'isnan',
'isqrt',
'ldexp',
'lgamma',
'log',
'log10',
'log1p',
'log2',
'modf',
'nan',
'perm',
'pi',
'pow',
'prod',
'radians',
'remainder',
'sin',
'sinh',
'sqrt',
'tan',
'tanh',
'tau',
'trunc']
'cosh' in dir(math)
True
Ejecutar un módulo como script
Cualquier archivo .py que contenga un módulo es esencialmente también un script de Python,
y no hay ninguna razón por la que no pueda ejecutarse como tal. En caso de que el módulo
contenga la llamada a una función, esta se ejecutaría. El problema viene dado cuando lo que se
quiere es importar el módulo y no ejecutar la función.
Cuando se importa un archivo .py como módulo, Python establece en la variable especial
__name__ en el nombre del módulo. Sin embargo, si un archivo se ejecuta como un script
independiente, __name__ se establece (creativamente) en la cadena __main__ .
Aprovechando esta situación, se puede discernir cuál es el caso en tiempo de ejecución y
modificar el comportamiento en consecuencia.
Los módulos suelen estar diseñados con la capacidad de ejecutarse como un script
independiente con el fin de probar la funcionalidad contenida en el módulo. Esto se conoce
como prueba unitaria. Por ejemplo, suponga que ha creado un módulo fact.py que contiene
una función factorial, de la siguiente manera:
def fact(n):
return 1 if n == 1 else n * fact(n-1)
if (__name__ == '__main__'):
import sys
if len(sys.argv) > 1:
print(fact(int(sys.argv[1])))
Este módulo se podría tanto importar para utilizar su función fact :
fact(7)
5040
!python ./ejercicios/fact.py 7
5040
%run ./ejercicios/fact.py 7
5040
• NumPy: Ofrece una estructura crítica para el almacenamiento y operaciones con datos: el
array multidimensional. NumPy es una librería de bajo nivel sobre la que se han desarrollado
otras.
• pandas: Ejemplo de librería desarrollada sobre NumPy. Ofrece dos estructuras de datos
basadas en el array NumPy: la serie (estructura unidimensional) y el DataFrame (estructura
bidimensional).
• SciPy: Esta librería ofrece herramientas matemáticas de todo tipo: resolución de ecuaciones
diferenciales, distribuciones, gestión de matrices...
• Matplotlib: Es la librería de visualización referencia en el entorno Python. Aun cuando
ofrece herramientas de bajo nivel y su uso no es especialmente amigable, sigue siendo
obligado su conocimiento, más cuando otras librerías de visualización se han construido
sobre ésta.
• Seaborn: Otra librería de visualización, en este caso desarrollada sobre Matplotlib. Mucho
más amigable que Matplotlib y con un estilo visual mucho más atractivo, es la primera
opción en muchos casos.
• Bokeh: Tercera librería de visualización de esta lista, aunque en este caso no está basada en
Matplotlib. Bokeh ofrece visualizaciones interactivas muy atractivas y útiles.
• Scikit-learn: Librería de referencia en el mundo del Machine Learning para Python. Ofrece
innumerables algoritmos y herramientas imprescindibles en cualquier proyecto de análisis
de datos.
• TensorFlow: TensorFlow ofrece herramientas para la definición y entrenamiento de redes
neuronales.
• Keras: Keras se ofrece como interfaz de alto nivel para librerías como TensorFlow, Theano o
CNTK.
• NLTK: Librería de procesamiento de lenguaje natural, con multitud de herramientas
orientadas al análisis de textos.
• XGBoost, LightGBM: Librerías que implementan los algoritmos homónimos, fundamentales
en entornos tabulares.
Errores y Excepciones
Hay (al menos) dos tipos distinguibles de errores: errores de sintaxis y excepciones. Los errores
de sintaxis, también conocidos como errores de interpretación, son quizás el tipo de error más
común que recibe mientras aún está aprendiendo Python y ocurren cuando el intérprete detecta
una declaración incorrecta:
El intérprete reproduce la línea responsable del error y muestra una pequeña “flecha” que
apunta al primer lugar donde se detectó el error. El error ha sido provocado (o al menos
detectado) en el elemento que precede a la flecha. Normalmente el intérprete también incluye
información referente al tipo de error producido.
Python tiene muchas excepciones integradas que responden a posibles problemas con la
ejecución del código. Algunas de las más comunes son:
Excepción Descripción
BaseException es la clase base común de todas las excepciones. Una de sus subclases,
Exception , es la clase base de todas las excepciones no fatales. Las excepciones que no son
subclases de Exception no se suelen manejar, porque se utilizan para indicar que el programa
debe terminar. Entre ellas se incluyen SystemExit , que es lanzada por sys.exit() y
KeyboardInterrupt , que se lanza cuando un usuario desea interrumpir el programa.
Cuando se produce una excepción, el proceso actual se detiene y pasa el error al proceso
llamador, repitiéndose esta acción hasta que el error se gestiona o el programa finaliza. Si no se
encuentra un gestor, se genera una unhandled exception (excepción no gestionada) y la
ejecución del código se interrumpe con un mensaje.
Al igual que ocurre con los errores sintácticos el intérprete propociona información de contexto
cuando se produce una excepción. Específicamente muestra el contexto donde ocurrió la
excepción, en forma de seguimiento de pila (Traceback), que en general, contiene un
seguimiento de pila que enumera las líneas de origen y un último mensajes con el nombre de la
excepción que se ha producido y una descpción del error vinculado a la misma.
def f1(x):
return f2(x)
def f2(x):
return f3(x)
def f3(x):
return float(x)
f1('123.456')
123.456
f1('hola')
---------------------------------------------------------------------------
ValueError Traceback (most recent call last)
<ipython-input-275-7421a1f21931> in <module>
----> 1 f1('hola')
<ipython-input-274-51f51e8ee670> in f1(x)
1 def f1(x):
----> 2 return f2(x)
3
4 def f2(x):
5 return f3(x)
<ipython-input-274-51f51e8ee670> in f2(x)
3
4 def f2(x):
----> 5 return f3(x)
6
7 def f3(x):
<ipython-input-274-51f51e8ee670> in f3(x)
6
7 def f3(x):
----> 8 return float(x)
9
10 f1('123.456')
El bloque try-except-else-finally
El control de excepciones se realiza mediante el bloque try-except :
try:
# hacer algo susceptible de generar un error
pass
except <Error>:
# manejar la excepción definida por <Error>
pass
Que funciona de la siguiente manera.
1. Se ejecuta la cláusula try , la(s) linea(s) entre las palabras reservadas try y except .
try:
float('hola')
except ValueError as zde:
print(f'Valor proporcionado no válido [{zde}]')
print('\n...resto de instrucciones...')
Valor proporcionado no válido [could not convert string to float: 'hola']
...resto de instrucciones...
try:
f1('hola')
except ValueError as zde:
print(f'Valor proporcionado no válido [{zde}]')
print('\n...resto de instrucciones...')
...resto de instrucciones...
El tratamiento de las excepciones, siempre que se pueda, debe ser lo más cercano
funcionalmente al momento en que estas se puedan generar.
def f3(x):
try:
return float(x)
except ValueError as zde:
print(f'Valor proporcionado no válido [{zde}]')
f3('hola')
print('\n...resto de instrucciones...')
...resto de instrucciones...
f1('hola')
print('\n...resto de instrucciones...')
...resto de instrucciones...
Cuando ocurre una excepción, puede tener un valor asociado, también conocido como el
argumento de la excepción. La presencia y el tipo de argumento depende del tipo de excepción.
Para mayor comodidad, los tipos de excepción integrados definen __str__() para imprimir
todos los argumentos sin acceder explícitamente a args .
def f3(x):
try:
return float(x)
except ValueError as zde:
print(f'{zde.args=}')
print(f'Valor proporcionado {x=} no es válido -> {zde}')
f1('hola')
print('\n...resto de instrucciones...')
...resto de instrucciones...
f3('hola')
El módulo traceback proporciona una interfaz estándar para extraer, formatear y mostrar
trazas de pilas de programas de Python. Dicho módulo copia el comportamiento del intérprete
de Python cuando muestra una traza de pila. Es útil a la hora de mostrar trazas de pilas bajo el
control del programa, como si de un wrapper alrededor del intérprete se tratara.
Una declaración try puede tener más de una cláusula except , para especificar gestores
para diferentes excepciones. Como máximo, se ejecutará un gestor. Además, una cláusula
except puede nombrar múltiples excepciones como una tupla entre paréntesis. e incluso no
indicar ninguna excepción en la cláusula except , en cuyo caso se trataría cualquier excepción
ocurrida:
try:
# hacer algo susceptible de generar un error
pass
except <Error1>:
# manejar la exception de tipo <Error1>
pass
except (<Error2>):
# manejar la exception de tipo <Error2>
pass
except (<Error3>, <Error4>, ...):
# manejar múltiples excepciones <Error3>, <Error4>
pass
except:
# manejar el resto de posibles excepciones no tratadas previamente
pass
Exception se puede utilizar como un comodín que atrapa (casi) todo. Sin embargo, es una
buena práctica ser lo más específico posible con los tipos de excepciones que pretendemos
manejar, y permitir que cualquier excepción inesperada se propague.
La declaración try…except tiene una cláusula else opcional, que, cuando está presente,
debe seguir todas las cláusulas except . Es útil para el código que debe ejecutarse si la
cláusula try no lanza una excepción:
try:
# hacer algo susceptible de generar un error
pass
except <Error>:
# manejar la exception de tipo <Error>
pass
else:
# se ejecuta cuando no se generan excepciones
pass
def f3(x):
try:
float(x)
except ValueError as zde:
print(f'Valor proporcionado {x=} no es válido -> {zde}')
except TypeError as te:
print(f'Tipo proporcionado, {type(x)}, no válido -> {te}')
else:
return float(x)
f3('123.456')
123.456
La declaración try…except tiene otra cláusula opcional cuyo propósito es definir acciones de
limpieza que serán ejecutadas bajo ciertas circunstancias. El bloque finally , si se especifica,
se ejecutará independientemente de si el bloque try genera un error o no:
try:
# hacer algo susceptible de generar un error
pass
except:
# maneja cualquier excepción
pass
finally:
# se ejecuta en cualquier caso
pass
Los siguientes puntos explican casos más complejos en los que se produce una excepción:
• Si ocurre una excepción durante la ejecución de la cláusula try , que no es gestionada por
una cláusula except , la excepción es relanzada después de que se ejecute el bloque de la
cláusula finally .
• Podría aparecer una excepción durante la ejecución de una cláusula except o else . De
nuevo, la excepción será relanzada después de que el bloque de la cláusula finally se
ejecute.
• Si una cláusula finally incluye una sentencia return , el valor retornado será el de la
cláusula finally , no la del de la sentencia return de la cláusula try .
def f3(x):
try:
float(x)
except ValueError as zde:
print(f'Valor proporcionado {x=} no es válido -> {zde}')
except TypeError as te:
print(f'Tipo proporcionado, {type(x)}, no válido -> {te}')
else:
return float(x)
finally:
print(f'Procesando float({repr(x)})')
f3('123.456')
Procesando float('123.456')
123.456
f3('hola')
Valor proporcionado x='hola' no es válido -> could not convert string to float: 'ho
la'
Procesando float('hola')
Lanzando excepciones
La declaración raise permite al programador forzar a que ocurra una excepción específica. El
único argumento de raise indica la excepción a lanzar. Debe ser una instancia de excepción o
una clase de excepción (una clase que derive de BaseException , como Exception o una
de sus subclases). Si se pasa una clase de excepción, se instanciará implícitamente llamando a
su constructor sin argumentos:
raise <Excepcion>(args)
def verificar_entero_par(valor):
if not isinstance (valor, int):
raise TypeError(f'El valor proporcionado {valor} no es entero')
elif valor % 2 != 0:
raise ValueError(f'El valor proporcionado {valor} no es par')
else:
return valor
verificar_entero_par(3.8)
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
<ipython-input-286-54f3ca0f9718> in <module>
----> 1 verificar_entero_par(3.8)
<ipython-input-285-49da99ff66ec> in verificar_entero_par(valor)
1 def verificar_entero_par(valor):
2 if not isinstance (valor, int):
----> 3 raise TypeError(f'El valor proporcionado {valor} no es entero')
4 elif valor % 2 != 0:
5 raise ValueError(f'El valor proporcionado {valor} no es par')
verificar_entero_par(3)
---------------------------------------------------------------------------
ValueError Traceback (most recent call last)
<ipython-input-287-2817bcfa48b9> in <module>
----> 1 verificar_entero_par(3)
<ipython-input-285-49da99ff66ec> in verificar_entero_par(valor)
3 raise TypeError(f'El valor proporcionado {valor} no es entero')
4 elif valor % 2 != 0:
----> 5 raise ValueError(f'El valor proporcionado {valor} no es par')
6 else:
7 return valor
El patrón más común para gestionar Exception es imprimir o registrar la excepción y luego
volver a re-lanzarla, permitiendo de este modo que otro llamador maneje la excepción:
def f3(x):
try:
return float(x)/0
except ValueError as zde:
print(f'Valor proporcionado {x=} no es válido -> {zde}')
except TypeError as te:
print(f'Tipo proporcionado, {type(x)}, no válido -> {te}')
except Exception as err:
print(f'Error no controlado:\n\t- {err=}, \n\t- {type(err)=}')
raise # relanza la excepción capturada en el bloque except
f3('123.456')
Error no controlado:
- err=ZeroDivisionError('float division by zero'),
- type(err)=<class 'ZeroDivisionError'>
---------------------------------------------------------------------------
ZeroDivisionError Traceback (most recent call last)
<ipython-input-288-9f32f670dfe3> in <module>
10 raise # relanza la excepción capturada en el bloque except
11
---> 12 f3('123.456')
<ipython-input-288-9f32f670dfe3> in f3(x)
1 def f3(x):
2 try:
----> 3 return float(x)/0
4 except ValueError as zde:
5 print(f'Valor proporcionado {x=} no es válido -> {zde}')
Las clases de excepción pueden ser definidas de la misma forma que cualquier otra clase, pero
es habitual mantenerlas lo más simples posible, a menudo ofreciendo solo un número de
atributos con información sobre el error que leerán los gestores de la excepción.
La mayoría de las excepciones se definen con nombres acabados en «Error», de manera similar
a la nomenclatura de las excepciones estándar. Muchos módulos estándar definen sus propias
excepciones para reportar errores que pueden ocurrir en funciones propias.
class EnteroParError(Exception):
def __init__(self, tipo, valor):
self.tipo = tipo
self.valor = valor
def __str__(self):
salida = f'El valor proporcionado {self.valor} '
if self.tipo.lower() == 'impar':
salida += 'no es par'
elif self.tipo.lower() == 'tipo':
salida += 'no es entero'
return salida
def verificar_entero_par(valor):
if not isinstance (valor, int):
raise EnteroParError('tipo', valor)
elif valor % 2 != 0:
raise EnteroParError('impar', valor)
else:
return valor
verificar_entero_par(3)
---------------------------------------------------------------------------
EnteroParError Traceback (most recent call last)
<ipython-input-291-2817bcfa48b9> in <module>
----> 1 verificar_entero_par(3)
<ipython-input-290-3a38ddc1c7f5> in verificar_entero_par(valor)
3 raise EnteroParError('tipo', valor)
4 elif valor % 2 != 0:
----> 5 raise EnteroParError('impar', valor)
6 else:
7 return valor
verificar_entero_par(2.0)
---------------------------------------------------------------------------
EnteroParError Traceback (most recent call last)
<ipython-input-292-7d5ff1bd5f09> in <module>
----> 1 verificar_entero_par(2.0)
<ipython-input-290-3a38ddc1c7f5> in verificar_entero_par(valor)
1 def verificar_entero_par(valor):
2 if not isinstance (valor, int):
----> 3 raise EnteroParError('tipo', valor)
4 elif valor % 2 != 0:
5 raise EnteroParError('impar', valor)
Comando %xmode
La mayoría de las veces, cuando un script de Python falla, generará una excepción. Cuando el
intérprete encuentra una de estas excepciones, la información sobre la causa del error se puede
encontrar en el rastreo, al que se puede acceder desde Python. Con la función mágica %xmode ,
IPython permite controlar la cantidad de información a mostrar cuando se genera la excepción.
El comando %xmode tiene un solo argumento, el modo, y hay tres posibilidades: simple
( Plain ), contextual ( Context ) y detallado ( Verbose ). El modo por defecto es contextual.
%xmode Plain
f1('hola')
Exception reporting mode: Plain
Traceback (most recent call last):
%xmode Verbose
f1('hola')
<ipython-input-274-51f51e8ee670> in f1(x='hola')
1 def f1(x):
----> 2 return f2(x)
global f2 = <function f2 at 0x7f1ef2c01790>
x = 'hola'
3
4 def f2(x):
5 return f3(x)
<ipython-input-274-51f51e8ee670> in f2(x='hola')
3
4 def f2(x):
----> 5 return f3(x)
global f3 = <function f3 at 0x7f1ef2c014c0>
x = 'hola'
6
7 def f3(x):
<ipython-input-293-e49e2ea69218> in f3(x='hola')
1 # f1() llama a f2() y este a su vez a f3()
2 def f3(x):
----> 3 return float(x)
global float = undefined
x = 'hola'
Jerarquía de excepciones
BaseException
├── BaseExceptionGroup
├── GeneratorExit
├── KeyboardInterrupt
├── SystemExit
└── Exception
├── ArithmeticError
│ ├── FloatingPointError
│ ├── OverflowError
│ └── ZeroDivisionError
├── AssertionError
├── AttributeError
├── BufferError
├── EOFError
├── ExceptionGroup [BaseExceptionGroup]
├── ImportError
│ └── ModuleNotFoundError
├── LookupError
│ ├── IndexError
│ └── KeyError
├── MemoryError
├── NameError
│ └── UnboundLocalError
├── OSError
│ ├── BlockingIOError
│ ├── ChildProcessError
│ ├── ConnectionError
│ │ ├── BrokenPipeError
│ │ ├── ConnectionAbortedError
│ │ ├── ConnectionRefusedError
│ │ └── ConnectionResetError
│ ├── FileExistsError
│ ├── FileNotFoundError
│ ├── InterruptedError
│ ├── IsADirectoryError
│ ├── NotADirectoryError
│ ├── PermissionError
│ ├── ProcessLookupError
│ └── TimeoutError
├── ReferenceError
├── RuntimeError
│ ├── NotImplementedError
│ └── RecursionError
├── StopAsyncIteration
├── StopIteration
├── SyntaxError
│ └── IndentationError
│ └── TabError
├── SystemError
├── TypeError
├── ValueError
│ └── UnicodeError
│ ├── UnicodeDecodeError
│ ├── UnicodeEncodeError
│ └── UnicodeTranslateError
└── Warning
├── BytesWarning
├── DeprecationWarning
├── EncodingWarning
├── FutureWarning
├── ImportWarning
├── PendingDeprecationWarning
├── ResourceWarning
├── RuntimeWarning
├── SyntaxWarning
├── UnicodeWarning
└── UserWarning
Ficheros y Sistema Operativo
Aunque para el procesamiento de datos se utilizan normalmente herramientas de alto nivel como
las proporcionadas por la librería Pandas para leer archivos de datos del disco en estructuras
de datos de Python. Sin embargo, es importante comprender los conceptos básicos de cómo
trabajar con archivos en Python.
Para abrir un archivo para leer o escribir, se usa la función incorporada open con una ruta de
archivo relativa o absoluta, un modo de apertura y una codificación.
La siguiente tabla muestra los diferentes modos de trabajo de los ficheros en Python:
Modo Descripción
r Solo lectura
r+ Lectura y escritura
t Modo texto (valor por defecto). Automáticamente decodifica los bytes a Unicode
La siguiente tabla muestra los métodos y atributos más habituales de los ficheros:
Método Descripción
Devuelve los datos del archivo como una cadena, el argumento opcional size
read([size])
indica el número de bytes para leer
Devuelve una lista con las líneas del archivo, el argumento opcional size indica
readlines([size])
el número de líneas para leer
seek(pos) Mueve el apuntador del fichero a la posición indicada por pos (entero)
La función open nos devuelve el manejador del ficheros que define un flujo de entrada/salida
para las operaciones de lectura/escritura. Este objeto almacena, entre otras cosas, la ruta al
fichero, el modo de apertura y la codificación.
path = './data/teoria/segismundo.txt'
f = open(path)
f
Las funciones read y readlines sino se proporciona el argumento opcional size leen
todo el contenido del fichero de una sola vez.
f.read()
Hay que tener en cuenta que, una vez abierto el fichero, la lectura de su contenido se puede
realizar una única vez.
f = open(path)
f.readlines()
f.close()
Python permite la lectura de ficheros línea a línea, iterando sobre el propio manejador del
fichero, ya que los ficheros son estructuras de datos iterables. También ofrece la función
readline que nos devuelve la siguiente línea del fichero cada vez que se llama:
f = open(path)
for linea in f:
print(linea.rstrip()) # strip elimina el caracter de fin-de-linea (EOL)
f.close()
y en el mundo, en conclusión,
todos sueñan lo que son,
aunque ninguno lo entiende.
f = open(path)
while (linea:=f.readline()):
print(linea.rstrip())
f.close()
Sueña el rico en su riqueza,
que más cuidados le ofrece;
y en el mundo, en conclusión,
todos sueñan lo que son,
aunque ninguno lo entiende.
f = open(path)
lineas = [linea.rstrip() for linea in f]
f.close()
lineas
Una de las maneras de facilitar la limpieza de archivos abiertos es manipular los ficheros dentro
de un contexto generado por el bloque with , que cierra automáticamente los archivos al
alcanzar el final del bloque:
with open(path) as f:
for linea in f:
print(linea.rstrip()) # strip elimina el caracter de fin-de-linea (EOL)
y en el mundo, en conclusión,
todos sueñan lo que son,
aunque ninguno lo entiende.
Como se ha indicado, el modo de trabajo por defecto de los archivos de Python (ya sea en
lectura o en escritura) es el modo de texto, lo que significa que se trabajará con cadenas de
Python (es decir, Unicode). Esto se contrapone con el modo binario, que se configura añadiendo
b en el modo de archivo.
'Sueña el r'
b'Sue\xc3\xb1a el '
'Sueña el '
El modo de texto, combinado con la opción de codificación encoding del método open ,
proporciona una manera conveniente de convertir de una codificación Unicode a otra. El módulo
sys de Python permite conocer información sobre la codificación utilizada por el sistema:
Método Descripción
import sys
print(f'defaultencoding: {sys.getdefaultencoding()}')
print(f'filesystemencoding: {sys.getfilesystemencoding()}')
defaultencoding: utf-8
filesystemencoding: utf-8
Las operaciones de escritura son similares a las de lectura. Para escribir texto en un fichero hay
que abrir dicho fichero en modo escritura.
path = './data/teoria/salida.txt'
f = open(path, 'w')
f
Una vez abierto el fichero para escritura podemos hacer uso de la función write para escribir
contenido en el mismo. Esta función no incluye el salto de línea por defecto, así que debemos
añadirlo de manera explícita.
f.write('primera línea\n')
f.write('segunda línea\n')
f.write('tercera línea\n')
f.close()
with open(path) as f:
for linea in f:
print(linea.rstrip()) # strip elimina el caracter de fin-de-linea (EOL)
primera línea
segunda línea
tercera línea
En caso de tener una lista de elementos con el formato adecuado de escritura, podemos hacer
uso de la función writelines que escribe todos los contenidos a la vez. La única diferencia
entre añadir información a un fichero y escribir información en un fichero es el modo de apertura
del fichero. En este caso utilizamos a por «append»:
lineas = (' cuarta línea\n', ' quinta línea\n', ' sexta línea\n')
with open(path, 'a') as f:
f.writelines(lineas)
with open(path) as f:
for linea in f:
print(linea.rstrip()) # strip elimina el caracter de fin-de-linea (EOL)
primera línea
segunda línea
tercera línea
cuarta línea
quinta línea
sexta línea
El módulo OS de Python
El módulo os nos permite acceder a funcionalidades dependientes del Sistema Operativo. Sobre
todo, aquellas que nos refieren información sobre el entorno del mismo y nos permiten manipular
la estructura de directorios. Entre los métodos más destacados de este módulo se encuentran:
Método Descripción
import os
for variable, valor in os.environ.items():
print("{0}: {1}".format(variable, valor))
El módulo os también nos provee del submódulo path (os.path) el cual nos permite acceder a
ciertas funcionalidades relacionadas con los nombres de las rutas de archivos y directorios.
Entre ellas, las más destacadas se describen en la siguiente tabla:
Método Descripción
Expresiones Regulares
Una expresión regular es una secuencia especial de caracteres que ayuda a encontrar otras
cadenas o conjuntos de cadenas que coincidan con patrones específicos; es un lenguaje
poderoso para hacer coincidir patrones de texto.
Se pueden utilizar diferentes sintaxis de expresiones regulares para extraer datos de archivos de
texto, XML, JSON, contenedores HTML, etc.
Las expresiones regulares pueden contener tanto caracteres especiales como ordinarios. La
mayoría de los caracteres ordinarios, como 'A', 'a', o '0' son las expresiones regulares más
sencillas; simplemente se ajustan a sí mismas. Se pueden concatenar caracteres ordinarios, así
que HOLA coincide con la cadena 'HOLA'. Algunos caracteres, como '|' o '(', son especiales. Los
caracteres especiales representan clases de caracteres ordinarios, o afectan a la forma en que
se interpretan las expresiones regulares que los rodean.
Patrón Descripción
[...] Coincide con cualquier carácter individual que esté entre los corchetes
[^...] Coincide con cualquier carácter individual que no esté entre los corchetes
re{n,m} Coincide con al menos n y como máximo m apariciones del expresión precedente
(?imx) Alterna temporalmente entre las opciones i, m o x dentro de una expresión regular
(?-imx:
Desactiva temporalmente las opciones i, m o x entre paréntesis.
re)
(?#...) Comentario.
(?! re) Especifica la posición usando la negación del patrón. no tiene rango.
Coincide con el final de la cadena, si existe una nueva línea, coincide justo antes de la
\Z
nueva línea
\b Coincide con los límites de las palabras cuando están fuera de los corchetes
\n, \t, etc. Coincide con nuevas líneas, retornos de carro, tabulaciones, etc.
Las expresiones regulares usan el carácter de barra inversa ('\') para indicar formas
especiales o para permitir el uso de caracteres especiales sin invocar su significado especial.
Cuando definimos una expresión regular es conveniente utilizar el formato raw en las cadenas
de texto para que los caracteres especiales no pierdan su semántica: r'cadena'.
Las expresiones regulares pueden ser concatenadas para formar nuevas expresiones regulares.
Algunas de las operaciones más habituales con expresiones regulares, implementadas en el
módulo re , son:
import re
CoursesData = \
"""101 COM Computers
205 MAT Mathematics
189 ENG English"""
Las coincidencias vacías se incluyen en el resultado. El resultado depende del número de grupos
detectados en el patrón. Si no hay grupos, retorna una lista de strings que coincidan con el
patrón completo. Si existe exactamente un grupo, retorna una lista de strings que coincidan con
ese grupo. Si hay varios grupos presentes, retorna una lista de tuplas de strings que coinciden
con los grupos. Los grupos que no son detectados no afectan la forma del resultado.
Las expresiones regulares pueden ser concatenadas para formar nuevas expresiones regulares.
En este caso definiendo grupos de búsqueda:
Si vamos a utilizar una expresión regular una única vez entonces no debemos preocuparnos por
cuestiones de rendimiento. Pero si repetimos su aplicación, sería más recomendable compilar la
expresión regular a un patrón para mejorar el rendimiento:
course_numbers_compiled = re.compile('[0-9]+')
print(type(course_numbers_compiled))
# Extrae todos los números de curso
course_numbers = re.findall(course_numbers_compiled, CoursesData)
print(course_numbers)
<class 're.Pattern'>
['101', '205', '189']
python
$ python
Python 3.7 (default, Sep 16 2015, 09:25:04)
[GCC 4.8.2] on linux
Type "help", "copyright", "credits" or "license" for more
information.
>>>
Dado que la elección del directorio donde vive el intérprete es una opción de instalación, otros
lugares son posibles; Consulte con su gurú de Python local o administrador del sistema. (Por
ejemplo, /usr/local/python es una ubicación alternativa popular).
En las máquinas Windows en las que haya instalado desde Microsoft Store, el comando
python3.7 estará disponible. Si tiene instalado el iniciador py.exe , puede usar el comando
py .
#!/usr/bin/env python3.7
$ chmod +x myscript.py
En los sistemas Windows, no hay noción de un "modo ejecutable". El instalador de Python asocia
automáticamente los archivos .py con python.exe, de modo que un doble clic en un archivo de
Python lo ejecutará como un script. La extensión también puede ser .pyw , en ese caso, se
suprime la ventana de la consola que aparece normalmente.
IPython
IPython es un shell interactivo que añade funcionalidades extra al modo interactivo incluido con
Python, como resaltado de líneas y errores mediante colores, una sintaxis adicional para el shell,
autocompletado mediante tabulador de variables, módulos y atributos; entre otras
funcionalidades. IPython proporciona una arquitectura rica para la computación interactiva con:
# !dir en Windows
!ls
# !cd en Windows
!pwd
/workspace/Curso Python
type(listado)
IPython.utils.text.SList
Los resultados no se devuelven como listas, sino como un tipo de retorno de shell especial
definido en IPython, que se parece mucho a una lista de Python, pero tiene funcionalidades
adicionales como los métodos grep y fields y las propiedades s, n y p que permiten buscar,
filtrar y mostrar los resultados de manera avanzada.
patron = "*.ipynb"
!ls {patron}
Adicionalmente IPython contiene una versión de los comandos de shell que le permiten
comportarse como una propia shell: %cd , %cat , %cp , %env , %ls , %man , %mkdir ,
%more , %mv , %pwd , %rm , y %rmdir . Cualquiera de estos comando mágicos se puede usar
sin el caracter % si el parámetro automagic se configura en on .
%ls *.ipynb
%lsmagic
• %reset que permite limpiar todo el namespace generado hasta ese momento, esto incluye
por supuesto variables, funciones, clases, etc...
• %who que nos permite ver todas las variables definidas.
• %run que nos permite ejecutar un fichero .py
• %load que nos permite cargar un fichero .py
%run ./ejercicios/fact.py 7
5040
# %load './data/teoria/segismundo.txt'
Sueña el rico en su riqueza,
que más cuidados le ofrece;
input([promp])
La función lee una línea de la entrada (generalmente del usuario), convierte la línea en una
cadena eliminando el caracter de final línea y la devuelve. Si se lee EOF , genera una excepción
EOFError .
Comando Descripción
h(elp) Muestra una lista de comandos o busca ayuda sobre un comando específico
f1('123.456')
123.456
%debug
> <ipython-input-293-e49e2ea69218>(3)f3()
1 # f1() llama a f2() y este a su vez a f3()
2 def f3(x):
----> 3 return float(x)
ipdb> a
x = 'hola'
ipdb> s
Si se desea que el depurador se inicie automáticamente cada vez que se genera una
excepción, puede usar la función mágica %pdb para activar este comportamiento
automático.
Temporización y profiling
Una vez que tenemos el código funcionando, puede ser útil profundizar un poco en su eficiencia.
A veces es útil comprobar el tiempo de ejecución de un comando o conjunto de comandos dado;
otras veces es útil profundizar en un proceso de varias líneas y determinar dónde se encuentra
el cuello de botella. IPython proporciona acceso a una amplia gama de funciones para este tipo
de sincronización y creación de perfiles de código. Entre ellas destacan:
• %time : Tiempo de ejecución de una sola instrucción
• %timeit : Tiempo de ejecución repetida de una sola declaración para mayor precisión
• %prun : Ejecuta código con el generador de perfiles
• %lprun : Ejecuta código con el generador de perfiles línea por línea
• %memit : Mide el uso de memoria de una sola declaración
• %mprun : Ejecutar código con el perfilador de memoria línea por línea
Los últimos cuatro comandos no están incluidos con IPython, necesitan de la instalación de
las extensiones line_profiler y memory_profiler.
%time sum(range(10000))
%timeit sum(range(10000))
134 µs ± 10.1 µs per loop (mean ± std. dev. of 7 runs, 10000 loops each)
%%timeit
total = 0
for i in range(1000):
for j in range(1000):
total += i*(-1)**j
250 ms ± 4.92 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
def sum_of_lists(N):
total = 0
for i in range(5):
L = [j^(j>>i) for j in range(N)]
total += sum(L)
return total
%prun sum_of_lists(1000000)
El perfil función por función de %prun es útil, pero a veces es más conveniente tener un
informe de perfil línea por línea. Esta funcionalidad no está integrada en Python ni en IPython,
pero hay un paquete line_profiler disponible para la instalación que la proporciona. Los
pasos a realizar para utilizar la funcionalidad son:
%load_ext line_profiler
%load_ext line_profiler
Otro aspecto de la creación de perfiles es la cantidad de memoria que utiliza una operación. Esto
se puede evaluar con otra extensión de IPython, memory_profiler . Los pasos a realizar para
utilizar la funcionalidad son:
%load_ext memory_profiler
%load_ext memory_profiler
%memit sum_of_lists(1000000)
Para una descripción línea por línea del uso de la memoria, podemos usar el comando mágico
%mprun . El único incoveniente es que solo para funciones definidas en módulos separados, no
se puede utilizar directamente desde el notebook.
Overwriting mprun_demo.py
Este gasto de memoria hay que añadirlo al que utiliza el propio intérprete de Python.