0% encontró este documento útil (0 votos)
85 vistas101 páginas

Python

El documento describe los elementos estructurales del lenguaje Python, incluyendo variables, tipos de datos, funciones, módulos, excepciones y E/S. También cubre conceptos como tipado dinámico, interpretación, multiparadigma y estilos de codificación.

Cargado por

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

Python

El documento describe los elementos estructurales del lenguaje Python, incluyendo variables, tipos de datos, funciones, módulos, excepciones y E/S. También cubre conceptos como tipado dinámico, interpretación, multiparadigma y estilos de codificación.

Cargado por

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

Estructura y elementos del lenguaje

• 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

Estructura y elementos del lenguaje


Dentro de los lenguajes informáticos, Python, pertenece al grupo de los lenguajes de
programación y puede ser clasificado como un lenguaje de alto nivel (creado a finales de los
80/principios de los 90), interpretado, de propósito general, multiplataforma, de tipado dinámico
y multiparadigma.

Lenguaje de alto nivel


Se refiere a la forma en la que le damos instrucciones a la computadora. Los lenguajes de bajo
nivel son los que están más cercanos al hardware de la computadora y, los de alto nivel, los que
están más lejanos. Un lenguaje de programación de alto nivel se caracteriza por expresar los
algoritmos de una manera adecuada a la capacidad cognitiva humana, en lugar de la capacidad
con que las máquinas lo ejecutan.

• 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.

La principal diferencia entre un lenguaje compilado y uno interpretado es que el lenguaje


compilado requiere un paso adicional antes de ser ejecutado, la compilación, que convierte el
código que escribes a lenguaje de máquina. Un lenguaje interpretado, por otro lado, es
convertido a lenguaje de máquina a medida que es ejecutado. Un compilador lee todas las
instrucciones y genera un resultado; un intérprete ejecuta y genera resultados línea a línea.

Código Interpretado Código Compilado

Es traducido a lenguaje máquina mientras es


Debe estar en lenguaje máquina antes de ejecutarse
ejecutado

Crea aplicaciones multiplataforma mientras el Se deben crear diferentyes versiones para los
SO cuente con un intérprete diferentes SO

Ejecución más lenta de programas por la


Ejecución más rápida al partir del código máquina
interpretación del código

Es necesario realizar el proceso de compilación cada


Se pueden realizar cambios en el código
vez que se cambia el código fuente

El código fuente se distribuye Se distribuye el código ejecutable

Suele corresponder con Open Source Suele corresponder con entornos propietarios

En general, un lenguaje compilado está optimizado para el momento de la ejecución, aunque


esto signifique una carga adicional para el programador. Por otro lado, un lenguaje interpretado
está optimizado para hacerle la vida más fácil al programador, aunque eso signifique una carga
adicional para la máquina.

Lenguaje de propósito general


Se pueden desarrollar aplicaciones prácticamente en todos los campos: Administración de
sistemas, Aplicaciones de escritorio, Desarrollo web, Desarrollo de juegos, Análisis de datos, IoT,
IA, ML, etc.

Lenguaje de tipado dinámico


Los lenguajes de tipado dinámico son aquellos (como Python) donde el intérprete asigna a las
variables un tipo durante el tiempo de ejecución basado en su valor en ese momento y en los
que una variable puede tomar valores de distintos tipos a lo largo de la ejecución. No es
necesario declarar las variables antes de su uso. En la práctica, por lo general se asocian los
lenguajes de tipado dinámico con lenguaje interpretados.

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):

• Hermoso es mejor que feo.


• Explícito es mejor que implícito.
• Lo simple es mejor que lo complejo.
• Complejo es mejor que complicado.
• Plano es mejor que anidado.
• Escaso es mejor que denso.
• La legibilidad cuenta.
• Los casos especiales no son lo suficientemente especiales como para romper las reglas.
• Aunque la practicidad le gana a la pureza.
• Los errores nunca deben pasar en silencio.
• A menos que se silencie explícitamente.
• Frente a la ambigüedad, rechace la tentación de adivinar.
• Debe haber una, y preferiblemente solo una, forma obvia de hacerlo.
• Aunque esa manera puede no ser obvia al principio a menos que seas holandés.
• Ahora es mejor que nunca.
• Aunque nunca suele ser mejor que ahora mismo.
• Si la implementación es difícil de explicar, es una mala idea.
• Si la implementación es fácil de explicar, puede ser una buena idea.
• Los espacios de nombres son una gran idea, ¡hagamos más de eso!

import this

The Zen of Python, by Tim Peters

Beautiful is better than ugly.


Explicit is better than implicit.
Simple is better than complex.
Complex is better than complicated.
Flat is better than nested.
Sparse is better than dense.
Readability counts.
Special cases aren't special enough to break the rules.
Although practicality beats purity.
Errors should never pass silently.
Unless explicitly silenced.
In the face of ambiguity, refuse the temptation to guess.
There should be one-- and preferably only one --obvious way to do it.
Although that way may not be obvious at first unless you're Dutch.
Now is better than never.
Although never is often better than *right* now.
If the implementation is hard to explain, it's a bad idea.
If the implementation is easy to explain, it may be a good idea.
Namespaces are one honking great idea -- let's do more of those!

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.

False class from or


None continue global pass
True def if raise
and del import return
as elif in try
assert else is while
async except lambda with
await finally nonlocal yield
break for not

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:

• Usar nombres para variables (ejemplo articulo).


• Usar verbos para funciones (ejemplo obtener_articulo()).
• Usar adjetivos para datos de tipo lógico (ejemplo disponible).

Semántica del lenguaje


Definición de bloques
Python usa espacios en blanco (tabuladores o espacios) para estructurar el código en lugar de
usar llaves como ocurre en muchos otros lenguajes (R, C ++, Java y Perl...).

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.

inicio de la estructura de control:


expresiones
expresiones
expresiones

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:

# Esto es un comentario de una sola línea


mi_variable = 15
"""Y este es
un comentario
de varias
líneas"""
mi_variable = 15 # Este es un comentario en línea

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

Ancho del código


Los programas suelen ser más legibles cuando las líneas no son excesivamente largas. La
longitud máxima de línea recomendada por la guía de estilo de Python es de 80 caracteres. En
caso de que queramos romper una línea de código demasiado larga, tenemos dos opciones:

factorial = 4 * 3 * 2 * 1

• Usar la barra invertida '\':

factorial = 4 * \
3 * \
2 * \
1

• Usar los paréntesis '('...')':

factorial = (4 *
3 *
2 *
1)

Estructuras de Control de Flujo


Python, como casi todos los lenguajes de programación, tiene diferentes componentes para
definir lógica condicional, bucles y otros conceptos de flujo de control estándar. A través de ellos
es posible modificar este flujo secuencial de jecución de instrucciones para que tome
bifurcaciones o repita ciertas instrucciones.

Sentencia if, elif y else


La sentencia if es uno de los tipos de instrucciones de flujo de control más conocidos. En su
versión más sencilla una sentencia if comprueba una condición, que si es cierta (se evalúa a
True ) hará que se ejecute el código del bloque definido.

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')

Positivo pero menor que 5

En Python no es necesario incluir paréntesis ( y ) al escribir condiciones, aunque a veces es


recomendable por claridad o por establecer prioridades.

Operador ternario
Es una versión del operador if-else en una sola línea. Su sintaxis es:

valor_si_cierto if condición else valor_si_falso

a = 5
b = 10
max = a if a > b else b
max

10

Operador walrus (operador morsa)


Introducido en la versión 3.8.x de Python permite asignar valores a una variable y evaluarla
dentro de una expresión:

if (variable := 2**2) > 5:


print(f"El resultado de la {variable} es mayor a 5")
else:
print(f"El número {variable} es menor a 5")

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')

File "<ipython-input-11-a7264c640702>", line 2


match valor:
^
SyntaxError: invalid syntax

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:

for variable in iterable:


<instrucciones> # suele incluir la manipulación de la 'variable'

for letra in "python":


print(letra, end="-")

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

Control de bucles, break , continue y pass

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.

for n in [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]:


print(n)
if n == 3:
break

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:

for i in [1, 2, 3]:


for j in "abc":
print(i, j, sep="", end=" ")

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.

edad = 35 # Número entero (int)


edad = 0b1101 # Número entero binario (int)
edad = 0o43 # Número entero octal (int)
edad = 0x23 # Número entero hexadecimal (int)
precio = 7435.28 # Número real (float)
resultado = 4+3j # Número complejo (complex)
num_test = 100_345_405 # equivalente a 100345405
verdadero = True # se corresponde con 1 en su representación numérica
falso = False # se corresponde 0 en su representación numérica
Para definir valores numéricos en diferentes bases también se pueden usar las funciones
bin() para la base binaria, oct() para la base octal y hex() para la base hexadecimal.

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

% Módulo (resto de la división entera)


Python ofrece la posibilidad de escribir una asignación aumentada mezclando la asignación y un
operador:

variable += valor # equivale a variable = variable + valor


variable -= valor # equivale a variable = variable - valor
variable *= valor # equivale a variable = variable * valor
Python es totalmente compatible con aritmética mixta: cuando un operador de aritmética binaria
tiene operandos de diferentes tipos numéricos, el operando con el tipo "más estrecho" se amplía
al tipo del otro operando. Es lo que se denomina conversión implícita.

Tipo 1 Tipo 2 Resultado

bool int int

bool float float

int float float

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).

Igualmente es importante tener en cuenta la prioridad de los distintos operadores:

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

<, <= menor, menor o igual

>, >= mayor, mayor o igual

is identidad de objeto

is not identidad de objeto negada

Python ofrece la posibilidad de ver si un valor está entre dos límites de manera directa:

limite_inferior <= variable <= limite_superior

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.

Operadores a nivel de bit


Entre los operadores a nivel de bit que Python utiliza, podemos encontrar los siguientes:

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.

Tipos secuencia (str, tuple, range, list)


Hay cuatro tipos de secuencias: cadenas, tuplas, rangos y listas. Existen tipos inmutables (su
contenido no puede cambiar) y tipos mutables (su contenido puede variar).

Operaciones comunes en tipos secuencia


Las operaciones en la siguiente tabla son compatibles con la mayoría de los tipos de secuencia,
tanto mutables como inmutables. En la tabla, s y t son secuencias del mismo tipo, n , i , j
y k son enteros y x es un objeto arbitrario que cumple con las restricciones de tipo y valor
impuestas por s .

Operación Significado

x in s True si un item de s es igual a x , sino False

x not in s False si un item de s es igual a x , sino True

s+t concatenación de s y t

s * n, n * s equivalente a repetir s , n veces

len(s) longitud de s

min(s) el item menor de s

max(s) el item mayor de s

s.index(x[, i[, índice de la primera ocurrencia de x en s (entre el índice i y antes del


j]]) índice j )

s.count(x) número de ocurrencias de x en s


Operación Significado

s[i] iésimo item de s , con origen en 0

s[i:j] porción de s desde i hasta la posición anterior a j

s[i:j:k] porción de s desde i hasta la posición anterior a j , con paso k

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.

Operaciones comunes en tipos secuencia mutables


En la tabla s es una instancia de un tipo de secuencia mutable, t es cualquier objeto iterable
y x es un objeto arbitrario que cumple con cualquier tipo y restricciones de valor impuestas por
s.

Operación Significado

s[i] = x el item en posición i de s es reemplazado por x

s[i:j] = t la porción de s desde i a j es reemplazada por los contenidos de t

del s[i:j] elimina los elementos de s desde i a j

s[i:j:k] = t la porción de s[i:j:k] es reemplazada por los contenidos de t

del s[i:j:k] elimina los elementos s[i:j:k] de la secuencia

s.append(x) añade x al final de la secuencia

s.clear() elimina todos los items de s

s.copy() crea una copia de s

s.extend(t) or s += t extiende s con los contenidos de t

s *= n actualiza s con su contenido repetido n veces

s.insert(i, x) inserta x en s en la posición indicada por i

s.pop([i]) recupera el item de la posición i y también lo elimina de s

s.remove(x) elimina el primer item de s dónde s[i] sea igual a x

s.reverse() invierte los items de s

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:

• Comillas simples: 'permite comillas "dobles" incrustadas'


• Comillas dobles: "permite las comillas 'simples' incrustadas"
• Comillas triples: '''Tres comillas simples' '' , """ Tres comillas dobles
"""
• Utilizando el constructor str(object='obj'[, encoding='utf-8',
errors='strict'])

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.

# se corresponde con al valor booleano 'False'


cadena_vacia = ''
cadena_vacia = str()

Secuencias de escape y expresiones literales


Python permite escapar el significado de algunos caracteres para conseguir otros resultados. Si
escribimos una barra invertida \ antes del carácter en cuestión, le otorgamos un significado
especial.

In [1]: msg = 'Primera línea\nSegunda línea\nTercera línea'


msg

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.

In [1]: msg = r'Primera línea\nSegunda línea\nTercera línea'


msg

Out[1]:
Primera línea\nSegunda línea\nTercera línea

El modificador 'r' es muy utilizado para la escritura de expresiones regulares.

Métodos para cadenas


Las cadenas implementan todas las operaciones de secuencia comunes, junto con los métodos
adicionales habituales en múltiples lenguajes.

In [1]: a = 'cadena'

In [2]: a.<Press Tab>


a.capitalize a.format a.isupper a.rindex a.strip
a.center a.index a.join a.rjust a.swapcase
a.count a.isalnum a.ljust a.rpartition a.title
a.decode a.isalpha a.lower a.rsplit a.translate
a.encode a.isdigit a.lstrip a.rstrip a.upper
a.endswith a.islower a.partition a.split a.zfill
a.expandtabs a.isspace a.replace a.splitlines
a.find a.istitle a.rfind a.startswith
Dentro del tratamiento de datos las más utilizadas son:

• 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()

['a', 'b', 'c', 'd', 'e', 'f', 'g']

texto = 'a-b-c-d-e-f-g'
texto.split('-')

['a', 'b', 'c', 'd', 'e', 'f', 'g']

• 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:

texto = ' 1234567890 '


texto.strip()

'1234567890'

• El método find() . Encuentra la posición de la primera aparición de una subcadena dentro


de la cadena:

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:

print(*objects, sep=' ', end='\n', file=None, flush=False)


Podemos imprimir todas las variables que queramos separándolas por comas. El separador por
defecto entre las variables es un espacio, podemos cambiar el carácter que se utiliza como
separador entre cadenas (parámetro sep ). El carácter de final de texto es un salto de línea,
podemos cambiar el carácter que se utiliza como final de texto (parámetro end ).

msg1 = 'Hola'
msg2 = 'Mundo'
print(msg1, msg2)
print(msg1, msg2, sep=', ')
print(msg1, msg2, sep='-', end='!!!')

Hola Mundo
Hola, Mundo
Hola-Mundo!!!

Interpolación de cadenas ( f-strings )

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}'

'Me llamo Antonio, tengo 45 años y soy fontanero'

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'

En versiones anteriores de Python estas operativas se podían realizar con el método


format().

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».

'abc' < 'abd'

True

Representaciones literales de objetos


Los métodos str y repr se utilizan para la representación como cadenas de texò de un
objeto. Son muy útiles si se desea una visualización rápida de variables para fines de depuración,
ya que puede convertir cualquier objeto en una cadena .

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.

La función chr() permite representar un carácter a partir de su código y la función ord()


permite obtener el código (decimal) de un carácter a partir de su representación:

codigo_letra_A = ord('A')
codigo_letra_A

65

letra_A = chr(codigo_letra_A)
letra_A

'A'

Para un almacenamiento eficiente de las cadenas, la secuencia de carácteres se convierte en un


conjunto de bytes. El proceso se conoce como codificación. Existen diferentes tipos de
codiciaciones, las populares son utf-8 y ascii , etc. Por defecto, Python usa la codificación
utf-8 o «8-bit Unicode Transformation Format», un formato de codificación de caracteres
Unicode e ISO 10646 que, como particularidad, utiliza símbolos de longitud variable. La diferncia
principal con ascii es que esta codificación contiene caracteres que fueron pensados para el
idioma ingles, Unicode contiene los caracteres de casi todos los alfabetos del mundo.

Usando el método encode () de la cadena, se pueden convertir cadenas sin codificar en


cualquier codificación compatible con Python. La sintaxis del método encode() es:

encode(encoding = 'UTF-8', errors = 'strict')


De igual forma para decodificar cadenas está el métdodo decode() , cuya síntaxis es:

decode(encoding = 'UTF-8', errors = 'strict')

# cadena Unicode
cadena = 'pythön!'
print('La cadena es:', cadena)

# codificación por defecto a utf-8


print('La versión codificada es:', cadena.encode())
# ignore error
print('La versión codificada (con ignore) es:', cadena.encode("ascii", "ignore"))
# replace error
print('La versión codificada (con replace) es:', cadena.encode("ascii", "replace"))

La cadena es: pythön!


La versión codificada es: b'pyth\xc3\xb6n!'
La versión codificada (con ignore) es: b'pythn!'
La versión codificada (con replace) es: b'pyth?n!'

La codificación y decodificación no modifican el contenido de la cadena, sólo su representación:

cadena = "Esto es una cadena de ejemplo...";


cadena = cadena.encode('utf_32');

# 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'))

La versión codificada es: b'\xff\xfe\x00\x00E\x00\x00\x00s\x00\x00\x00t\x00\x00\x00


o\x00\x00\x00 \x00\x00\x00e\x00\x00\x00s\x00\x00\x00 \x00\x00\x00u\x00\x00\x00n\x0
0\x00\x00a\x00\x00\x00 \x00\x00\x00c\x00\x00\x00a\x00\x00\x00d\x00\x00\x00e\x00\x0
0\x00n\x00\x00\x00a\x00\x00\x00 \x00\x00\x00d\x00\x00\x00e\x00\x00\x00 \x00\x00\x00
e\x00\x00\x00j\x00\x00\x00e\x00\x00\x00m\x00\x00\x00p\x00\x00\x00l\x00\x00\x00o\x0
0\x00\x00.\x00\x00\x00.\x00\x00\x00.\x00\x00\x00'
La versión decodificada es: Esto es una cadena de ejemplo...

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:

• Usando un par de paréntesis para denotar la tupla vacía: ()


• Usando una coma para definir una tupla singleton: a, o (a,)
• Separando elementos con comas: a, b, c o (a, b, c)
• Usando el constructor tuple () o tuple (iterable)

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.

tupla_vacia = () # evaluable como 'False'


tupla_vacia = tuple() # evaluable como 'False'
tupla_vacia

()

una_tupla = 4, 5, 6, 7
una_tupla

(4, 5, 6, 7)

tupla_anidada = (4, 5, 6), (7, 8)


tupla_anidada

((4, 5, 6), (7, 8))

otra_tupla = tuple('cadena')
otra_tupla

('c', 'a', 'd', 'e', 'n', 'a')

# Acceso a los elementos, las secuencias comienzan en el índice de 0 en Python


otra_tupla[0]

'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'

TypeError: 'tuple' object does not support item assignment

# Se pueden concatenar tuplas utilizando el operador + para producir tuplas más largas
(4, None, 'foo') + (6, 0) + ('bar',)

(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

('foo', 'bar', 'foo', 'bar', 'foo', 'bar', 'foo', 'bar')

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=}')

a=1, b=2, c=3


a=4, b=5, c=6
a=7, b=8, c=9

En las nuevas versiones de Python se puede utilizar un desempaquetado avanzado utilizando la


sintaxis especial *rest que captura los resto de la tupla en una variable. Esta sintaxis también
se usa en funciones para capturar una lista arbitrariamente larga de argumentos posicionales:

valores = 1, 2, 3, 4, 5
primer_valor, segundo_valor, *resto_valores = valores
print(f'{primer_valor=}, {segundo_valor=}, {resto_valores=}')

primer_valor=1, segundo_valor=2, resto_valores=[3, 4, 5]

primer_valor, *valores_intermedios, ultimo_valor = valores


print(f'{primer_valor=}, {valores_intermedios=}, {ultimo_valor=}')

primer_valor=1, valores_intermedios=[2, 3, 4], ultimo_valor=5

A la hora de desempaquetar los valores de una tupla si no es necesario recuperar parte de la


información porque no es útil para el proceso, por convención se usa el guión bajo ( _ ) para su
denominación:

primer_valor, *_, ultimo_valor = valores


print(f'{_=}')

_=[2, 3, 4]

El desempaquetado de tuplas es extensible a cualquier tipo de datos que sea iterable.

Métodos para tuplas


Dado que el tamaño y el contenido de una tupla no se pueden modificar, no tiene muchos
métodos de instancia. Uno particularmente útil (también disponible en listas) es count() , que
cuenta el número de ocurrencias de un valor:

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.

range ([start,] stop[, step])

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)

for valor in rango:


print(valor, end=" ")

0 1 2 3 4 5 6 7 8 9

rango = range(5, 0, -1)


for valor in rango:
print(valor, end=" ")

5 4 3 2 1

rango = range(-1, -10, -1)


for valor in rango:
print(valor, end=" ")

-1 -2 -3 -4 -5 -6 -7 -8 -9

-15 in rango

False

rango.index(-7)

rango[0:3]

range(-1, -4, -1)

rango[-1]

-9

range(0)

range(0, 0)

tupla = ('c', 'a', 'd', 'e', 'n', 'a')


for i in range(len(tupla)):
print('{0} -> {1}'.format(i, tupla[i]))
0 -> c
1 -> a
2 -> d
3 -> e
4 -> n
5 -> a

La compración de igualdad de rangos con == y != se realiza a nivel de secuencias. Es decir,


dos objetos de rango se consideran iguales si presentan la misma secuencia de valores.

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:

• Usando un par de corchetes para denotar la lista vacía: []


• Usando corchetes, separando elementos con comas: [a] , [a, b, c]
• Usando una expresión de comprensión: [x para x en iterable]
• Usando el constructor list () o list (iterable)

lista_vacia = list()
lista_vacia # evaluable como 'False'

[]

una_lista = [2, 3, 7, None]


una_lista

[2, 3, 7, None]

una_lista = list(range(10))
una_lista

[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

una_lista = [True, "2", 3.0, 4]


una_lista

[True, '2', 3.0, 4]

tupla = ('foo', 'bar', 'baz')


una_lista = list(tupla)
una_lista

['foo', 'bar', 'baz']

una_lista[1] ='peekaboo'
una_lista

['foo', 'peekaboo', 'baz']

una_lista.append('dwarf')
una_lista

['foo', 'peekaboo', 'baz', 'dwarf']

una_lista.insert(3, 'root')
una_lista
['foo', 'peekaboo', 'baz', 'root', 'dwarf']

una_lista.pop(2)

'baz'

una_lista

['foo', 'peekaboo', 'root', 'dwarf']

una_lista.remove('root')
una_lista

['foo', 'peekaboo', 'dwarf']

'bar' in una_lista

False

'root' not in una_lista

True

# El operador + concatena listas, pero es más rápido utilizar el método extend


una_lista.extend(['root', 'pie', 'body'])
una_lista

['foo', 'peekaboo', 'dwarf', 'root', 'pie', 'body']

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'

Métodos para listas


Las listas también proporcionan el siguiente método adicional sort (*, key = None,
reverse = False) que ordena la lista in-situ (modificándola), utilizando solo comparaciones
con el operador < entre elementos. El parámetro key permite especificar una función para
realizar la comparación de cada elemento de la lista y el parámetro reverse , si se establece en
True , invierte el resultado de la comparación.

otra_lista = [7, 2, 5, 1, 3]

otra_lista.sort()
otra_lista

[1, 2, 3, 5, 7]

otra_lista = ['saw', 'small', 'He', 'foxes', 'six']


otra_lista.sort(key=len)
otra_lista
['He', 'saw', 'six', 'small', 'foxes']

Métodos para secuencias

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):

for i, valor in enumerate(coleccion):


# hacer algo con valor

una_lista = ['foo', 'bar', 'baz']


for i, v in enumerate(una_lista):
print(i, " -> ", v)

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:

secuencia_1 = [100, 101, 102]


secuencia_2 = ['foo', 'bar', 'baz']
zipped = zip(secuencia_1, secuencia_2)
zipped

<zip at 0x7f1fe9ecd800>

La función zip produce un iterador:


for a, b in zipped:
print(f'({a}, {b})')

(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:

zipped = zip(secuencia_1, secuencia_2)


list(zipped)

[(100, 'foo'), (101, 'bar'), (102, 'baz')]

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:

secuencia_3 = [False, True]


list(zip(secuencia_1, secuencia_2, secuencia_3))

[(100, 'foo', False), (101, 'bar', True)]

Un uso muy común de zip es iterar simultáneamente en múltiples secuencias, combinando su


uso con enumerate :

for i, (a, b) in enumerate(zip(secuencia_1, secuencia_2)):


print(f'{i}: ({a}, {b})')

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:

jugadores = [('Nolan', 'Ryan'), ('Roger', 'Clemens'),


('Schilling', 'Curt', 'Smith')]

nombres, apellidos = zip(*jugadores)


print(f'{nombres=}, {apellidos=}')

nombres=('Nolan', 'Roger', 'Schilling'), apellidos=('Ryan', 'Clemens', 'Curt')

reversed

Itera sobre los elementos de una secuencia en orden inverso:

reversed(range(10))

<range_iterator at 0x7f1f9901f810>

list(reversed(range(10)))

[9, 8, 7, 6, 5, 4, 3, 2, 1, 0]

Tipos conjunto (set, frozenset)


Un objeto conjunto es una colección desordenada de objetos distintos susceptibles de tener
un valor de tipo hash . Los usos comunes incluyen la prueba de membresía, la eliminación de
duplicados de una secuencia y el cálculo de operaciones matemáticas como intersección, unión,
diferencia y diferencia simétrica. Al ser una colección desordenada, los conjuntos no registran la
posición del elemento ni el orden de inserción. En consecuencia, los conjuntos no son
compatibles con la indexación, la segmentación u otro comportamiento similar a una
secuencia.

Las conjuntos se pueden construir de varias maneras:

• Usando llaves, separando elementos con comas: {a} , {a, b, c}


• Usando una expresión de comprensión: {x para x en iterable}
• Usando el constructor set () o set (iterable) para conjuntos mutables
• Usando el constructor frozenset () o frozenset (iterable) para conjuntos
inmutables

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.add(x) --- Añade el elmento x al conjunto a

Restaure el conjunto a un estado vacío,


a.clear() ---
descartando todos sus elementos

a.copy() --- Crea una copia del conjunto a

Elimina el elemento x del conjunto a , generando


a.remove(x) ---
un error si el x no está en a

a.discard(x) --- Elimina el elemento x del conjunto a

Elimina un elemento arbitrario del conjunto a ,


a.pop() ---
generando un error si el conjunto está vacío

Define un conjunto con todos los elementos


a.union(b) a|b
diferenciados de a y b

Actualiza a con todos los elementos diferenciados


a.update(b) a |= b
de a y b

Define un conjunto con todos los elementos que


a.intersection(b) a&b
están en a y b , en ambos conjuntos

Actualiza a con todos los elementos que están en


a.intersection_update(b) a &= b
a y b , en ambos conjuntos

Define un conjunto con los elementos que están en


a.difference(b) a-b
a y no en b

Actualiza a con todos los elementos que están en


a.difference_update(b) a -= b
a y no en b
Sintaxis
Función Descripción
alt.

Define un conjunto con los elementos que están en


a.symmetric_difference(b) a^b
a o en b , pero no en ambos

Actualiza a con los elementos que están en a o


a.symmetric_difference_update(b) a ^= b
en b , pero no en ambos

True si todos los elementos de a están


a.issubset(b) a<b
contenidos en b

True si todos los elementos de b están


a.issuperset(b) a>b
contenidos en a

a.isdisjoint(b) --- True si a y b no tienen elementos en común

Actualiza a añadiendo todos los elementos


a.update(*otros) ---
presentes enlos cojuntos definidos por otros

conjunto_vacio = set() # se evalúa como 'False'

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}

Tipos mapa (dict)


Un objeto de tipo mapa permite asignar valores hash a objetos arbitrarios. Los mapas son
objetos mutables. Actualmente solo hay un tipo de mapa estándar, el diccionario. El diccionario
es probablemente la estructura de datos incorporada en Python más importante. Un nombre
más común para los diccionarios es mapa hash o array asociativo. Es una colección de pares de
clave-valor de tamaño flexible, donde la clave y el valor son objetos de Python.

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.

Las diccionarios se pueden construir de varias maneras:

• Usando un par de llaves para denotar un diccionario vacío: {}


• Usando pares clave-valor separados por comas entre llaves: {c:v, c:v, ...}
• Usando una expresión de comprensión: {c:v para c, v en iterable}
• Usando el constructor dict () o dict (secuencia claves-valor)

dicc_vacio = {} # se evalúa como 'False'


dicc_vacio = dict() # se evalúa como 'False'
dicc_vacio

{}

un_diccionario = {'sape': 4139, 'guido': 4127, 'jack': 4098}


un_diccionario

{'sape': 4139, 'guido': 4127, 'jack': 4098}

un_diccionario = dict([('sape', 4139), ('guido', 4127), ('jack', 4098)])


un_diccionario

{'sape': 4139, 'guido': 4127, 'jack': 4098}

un_diccionario = dict(sape=4139, guido=4127, jack=4098)


un_diccionario

{'sape': 4139, 'guido': 4127, 'jack': 4098}

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['dummy'] = 'otro valor'


un_diccionario

{'sape': 4139, 'guido': 4127, 'jack': 4098, 'dummy': 'otro valor'}

Si la clave existe en el diccionario, se reemplaza el valor correspondiente por el nuevo. Si la


clave es nueva, se añade al diccionario con el valor proporcionado.

Si accedemos a una clave que no existe se genera un error:

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 :

un_diccionario[5] = 'un valor'


un_diccionario['dummy'] = 'otro valor'
un_diccionario

{'sape': 4139,
'guido': 4127,
'jack': 4098,
'dummy': 'otro valor',
5: 'un valor'}

del un_diccionario['dummy']
un_diccionario

{'sape': 4139, 'guido': 4127, 'jack': 4098, 5: 'un valor'}

valor = un_diccionario.pop('guido')
valor

4127

un_diccionario

{'sape': 4139, 'jack': 4098, 5: 'un valor'}

En ambos casos si la clave que pretendemos eliminar/extraer no existe, obtendremos un


error.

un_diccionario

{'sape': 4139, 'jack': 4098, 5: 'un valor'}


un_diccionario.clear()
un_diccionario

{}

Sobre las claves de los diccionarios


Si bien los valores de un diccionario pueden ser cualquier objeto Python, las claves
generalmente tienen que ser objetos inmutables como tipos escalares (int, float, string) o tuplas
(todos los objetos en la tupla también deben ser inmutables). El término técnico es
hashability . Se puede verificar si un objeto es hashable (se puede usar como una clave
en un diccionario) con la función hash :

hash('cadena')

-1179683392021716786

hash((1, 2, (2, 3)))

-9209053662355515447

hash((1, 2, [2, 3])) # falla porque las listas son mutables

---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
<ipython-input-119-a5a61418fc8e> in <module>
----> 1 hash((1, 2, [2, 3])) # falla porque las listas son mutables

TypeError: unhashable type: 'list'

# 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}

Creando diccionarios a partir de secuencias


Una de las operativas más habituales con diccionarios es emparejar dos secuencias de forma
inteligente. Una primera aproximación podría ser:

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:

palabras = ['apple', 'bat', 'bar', 'atom', 'book']


un_diccionario = dict(zip(range(len(palabras)), palabras))
un_diccionario

{0: 'apple', 1: 'bat', 2: 'bar', 3: 'atom', 4: 'book'}

Métodos para Diccionarios


Claves y valores. Los métodos keys() y values() proporcionan iteradores de las claves y
valores del diccionario, respectivamente. Si bien los pares clave-valor no están en ningún orden
en particular, estas funciones devuelven las claves y los valores en el mismo orden. También hay
un método items () que devuelve una lista de tuplas (clave, valor) , que es la forma
más eficiente de examinar todos los datos de valores clave en el diccionario. Todas estas listas
se pueden pasar a la función sort() :

list(un_diccionario.keys())

[0, 1, 2, 3, 4]

list(un_diccionario.values())

['apple', 'bat', 'bar', 'atom', 'book']

list(un_diccionario.items())

[(0, 'apple'), (1, 'bat'), (2, 'bar'), (3, 'atom'), (4, 'book')]

Actualización: update El método update permite fusionar un diccionario con otro. El


método cambia los diccionarios, por lo que cualquier clave existente en los datos que se pasan
en la actualización descartarán sus valores antiguos.

un_diccionario = dict(sape=4139, guido=4127, jack=4098)


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

un_diccionario.update(otro_diccionario)
un_diccionario

{'sape': 4139, 'guido': 4127, 'jack': 4098, 'a': 1, 'b': 2, 'c': 3}

También es posible realizar la operación sin modificar los diccionarios originales haciendo uso
del operador ** :

{**un_diccionario, **otro_diccionario}

{'sape': 4139, 'guido': 4127, 'jack': 4098, 'a': 1, 'b': 2, 'c': 3}

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

TypeError: unsupported operand type(s) for |: 'dict' and 'dict'

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:

valor = diccionario.get (clave, valor_por_defecto)


El método get por defecto devolverá None si la clave no está presente, mientras que pop
provocará una excepción.

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:

palabras = ['apple', 'bat', 'bar', 'atom', 'book']


por_letras = {}
for palabra in palabras:
letra = palabra[0]
if letra not in por_letras:
por_letras[letra] = [palabra]
else:
por_letras[letra].append(palabra)
por_letras

{'a': ['apple', 'atom'], 'b': ['bat', 'bar', 'book']}

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

{'a': ['apple', 'atom'], 'b': ['bat', 'bar', 'book']}

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.

ChainMap - la lista de diccionarios


Introducido en Python 3.3, ChainMap le permite combinar múltiples asignaciones (diccionarios,
por ejemplo) en una. Esto es especialmente útil cuando se combinan varios contextos. Es una
clase similar a dict para crear una vista única de múltiples mapas, incorporados por
referencia, de modo que si alguno se actualiza esos cambios se reflejarán en ChainMap.

from collections import ChainMap


baseline = {'music': 'bach', 'art': 'rembrandt'}
adjustments = {'art': 'van gogh', 'opera': 'carmen'}
combined = ChainMap(baseline, adjustments)
print(combined)
print(combined['art'])

ChainMap({'music': 'bach', 'art': 'rembrandt'}, {'art': 'van gogh', 'opera': 'carme


n'})
rembrandt

Counter - seguimiento de los elementos más frecuentes


Un contador es una clase para realizar un seguimiento del número de apariciones de un
elemento. La clase Counter es una subclase de dict para contar objetos hashables. Es una
colección donde los elementos se almacenan como claves de diccionario y sus conteos como
valores de diccionario. Se permite que los conteos sean cualquier valor entero, incluidos cero o
valores negativos.
from collections import Counter
c = Counter(['eggs', 'ham', 'eggs'])
print(c['bacon'])
print(c['eggs'])

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.

c = Counter(a=4, b=2, c=0, d=-2)


sorted(c.elements())

['a', 'a', 'a', 'a', 'b', 'b']

• 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)

[('a', 5), ('b', 2), ('r', 2)]

• subtract([iterable-o-mapping]), los elementos se restan de un iterable o de otro mapeo (o


contador). Como dict.update() pero resta los conteos en lugar de reemplazarlos. Tanto
las entradas como las salidas pueden ser cero o negativas.

c = Counter(a=4, b=2, c=0, d=-2)


d = Counter(a=1, b=2, c=3, d=4)
c.subtract(d)
c

Counter({'a': 3, 'b': 0, 'c': -3, 'd': -6})

deque - la cola de dos extremos


El objeto deque (abreviatura de Double Ended Queue - "cola de dos extremos”) es una de las
colecciones más antiguas, fue introducido en Python 2.4 y son una generalización de pilas y
colas. Admiten hilos seguros, appends y pops eficientes en memoria desde cualquier lado
con aproximadamente el mismo rendimiento O(1) en cualquier dirección.

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.

from collections import deque


d = deque('ghi')
d.append('j')
d.appendleft('f')
d

deque(['f', 'g', 'h', 'i', 'j'])


defaultdict - diccionario con un valor predeterminado
La clase defaultdict es una subclase de dict que recibe como parámetro un tipo o una
función para generar el valor predeterminado de cada entrada en el diccionario. En lugar de
tener que verificar la existencia de una clave y agregar un valor cada vez, puede simplemente
declarar el valor predeterminado desde el principio, y no hay necesidad de preocuparse por el
resto.

from collections import defaultdict


palabras = ['apple', 'bat', 'bar', 'atom', 'book']
por_letras = defaultdict(list)
for palabra in palabras:
por_letras[palabra[0]].append(palabra)
por_letras

defaultdict(list, {'a': ['apple', 'atom'], 'b': ['bat', 'bar', 'book']})

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() .

namedtuple - tuplas con nombres de campo


El objeto namedtuple es exactamente lo que implica su nombre: una tupla con un nombre. Las
tuplas con nombre asignan significado a cada posición en una tupla y permiten un código más
legible y autodocumentado. Se pueden usar donde se usen tuplas regulares y agregan la
capacidad de acceder a los campos por nombre en lugar del índice de posición.

from collections import namedtuple


Punto = namedtuple('Punto', ['x', 'y', 'z'])
punto_a = Punto(1, 2, 3)
print(punto_a)
print(punto_a.z)

Punto(x=1, y=2, z=3)


3

Otras estructuras de datos de interés


Existen otros paquetes que proporcionan estructuras de datos interesantes que permiten, entre
otras posibilidades, trabajar con tipos enumerados o con colas con prioridad o listas ordenadas.

enum - un grupo de constantes


El paquete enum es bastante similar a namedtuple pero tiene un objetivo y una interfaz
completamente diferentes. El objeto básico de enum hace que sea realmente fácil tener
constantes en sus módulos mientras evita los números mágicos. Una enumeración es un
conjunto de nombres simbólicos (miembros) ligados a valores únicos y constantes. Dentro de
una enumeración, los miembros pueden compararse por identidad y la enumeración en sí puede
recorrerse.

from enum import Enum


class Color(Enum):
RED = 1
GREEN = 2
BLUE = 3

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>

heapq - lista con prioridad


El módulo heapq es un pequeño módulo que hace que sea muy fácil crear una cola de
prioridad en Python (implementación del algoritmo de montículos). Una estructura que siempre
hará que el elemento más pequeño (o más grande, según la implementación) esté disponible con
el mínimo esfuerzo. Las heapq son árboles binarios para los que cada nodo padre tiene un
valor menor o igual que cualquiera de sus hijos, sin embargo, esta implementación utiliza
matrices, para las cuales heap[k] <= heap[2*k+1] y heap[k] <= heap[2*k+2] para
todo k , contando los elementos desde cero. Para poder comparar, los elementos inexistentes
se consideran infinitos. La propiedad interesante de un montículo es que su elemento más
pequeño es siempre la raíz, heap[0] .

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.

from heapq import heappush, heappop


h = []
heappush(h, (5, 'write code'))
heappush(h, (7, 'release product'))
heappush(h, (1, 'write spec'))
heappush(h, (3, 'create tests'))
heappop(h)

(1, 'write spec')

bisect – la lista ordenada


Este módulo brinda soporte para mantener una lista ordenada sin tener que reordenar la lista
tras cada nueva inserción. Para listas largas de elementos que tienen operaciones de
comparación costosas, será una mejora respecto a la estrategia más habitual. El módulo se llama
bisect porque usa un algoritmo de bisección básico para lograr su objetivo.

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.

# Using the regular sort:


sorted_list = []
sorted_list.append(5) # O(1)
sorted_list.append(3) # O(1)
sorted_list.append(1) # O(1)
sorted_list.append(2) # O(1)
sorted_list.sort() # O(n * log(n)) = O(4 * log(4)) = O(8)
sorted_list
[1, 2, 3, 5]

[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

variable = {'a':3, 'b':5, 'c':6}


type(variable)

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:

variable = [True, "2", 3.0, 4]


[type(item) for item in variable]

[bool, str, float, int]

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:

x = y # No hace una copia de 'y' en 'x'


x = y # Hace que 'x' referencie al objeto referenciado por 'y'

En Python cuando se declara una variable el intérprete le asigna un identificador único


interno que se corresponde con un número entero que el sistema utiliza para diferenciarla de
otras variables (y de otros objetos) en la memoria. Dicho identificador se genera en cada
ejecución y se puede obtener con la función id.

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:

Tipos de datos inmutables: números, cadenas y tuplas


El intérprete de Python tiene un mecanismo de optimización que de forma automática no sólo
comparte direcciones entre variables sino contenidos mientras éstos no varían. Los objetos
inmutables no permiten el cambio sin generar una nueva instancia en memoria de la variable que
los referencia.

# comportamiento idéntico
# a = 100
# b = 100
a = 100
b = a

# a y b comparten dirección/id y contenido


print("a =",a)
print("b =",b)
print("id(a) =",id(a))
print("id(b) =",id(b))

a = 100
b = 100
id(a) = 94892117678432
id(b) = 94892117678432

a = 0 # se crea una nueva variable

# a y b tienen dirección/id y contenidos distintos


print("a =",a)
print("b =",b)
print("id(a) =",id(a))
print("id(b) =",id(b))

a = 0
b = 100
id(a) = 94892117675232
id(b) = 94892117678432

Tipos de datos mutables: listas, conjuntos y diccionarios


Los objetos mutables comparten las direcciones y los datos.

a = [1, 2, 3]
b = a

# a y b comparten dirección/id y contenido


print("a =",a)
print("b =",b)
print("id(a) =",id(a))
print("id(b) =",id(b))

a = [1, 2, 3]
b = [1, 2, 3]
id(a) = 139773700955968
id(b) = 139773700955968

a.append(4)

# a y b comparten dirección/id y contenido


print("a =",a)
print("b =",b)
print("id(a) =",id(a))
print("id(b) =",id(b))

a = [1, 2, 3, 4]
b = [1, 2, 3, 4]
id(a) = 139773700955968
id(b) = 139773700955968

Objetos mutables dentro de objetos inmutables:

a = (1, 2, [3, 4])


b = a
# a y b comparten dirección/id y contenido
print("a =",a)
print("b =",b)
print("id(a) =",id(a))
print("id(b) =",id(b))

a = (1, 2, [3, 4])


b = (1, 2, [3, 4])
id(a) = 139773700700800
id(b) = 139773700700800

b[2][0] = 5

# a y b comparten dirección/id y contenido


print("a =",a)
print("b =",b)
print("id(a) =",id(a))
print("id(b) =",id(b))

a = (1, 2, [5, 4])


b = (1, 2, [5, 4])
id(a) = 139773700700800
id(b) = 139773700700800

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 = [1, 2, [3, 4]]


b = a[:] # copia superficial
# 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))

a = [1, 2, [3, 4]]


b = [1, 2, [3, 4]]
id(a) = 139775045318208
id(b) = 139775045377856

a[0] = 0

# 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))

a = [0, 2, [3, 4]]


b = [1, 2, [3, 4]]
id(a) = 139775045318208
id(b) = 139775045377856

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))

a = [0, 2, [3, 5]]


b = [1, 2, [3, 5]]
id(a) = 139775045318208
id(b) = 139775045377856

Operaciones de copia superficial y profunda


Para evitar errores se recomienda el uso de las funciones que proporciona el módulo copy de la
librería estándar de Python. Este módulo consta de funciones para duplicar variables y otros
objetos con distinto nivel del profundidad: copy para copias superficiales y deepcopy para
copias profundas.

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))

a = [0, 2, [3, 5]]


b = [1, 2, [3, 4]]
id(a) = 139773687791744
id(b) = 139775045348032

Órdenes de complejidad (resumen)


Las computadoras son cada vez más rápidas y cuentan cada vez con más núcleos de ejecución,
de forma que un código poco eficiente puede "admitirse" con una capacidad de cómputo alta.
Sin embargo, en el contexto de la ciencia de datos, los conjuntos de datos pueden ser muy
grandes (p. ej., en 2014, Google publicó 30.000.000.000.000 de páginas, cubriendo 100.000.000
GB), por lo que es posible que soluciones poco eficientes simplemente no se adapten al tamaño
de manera aceptable.
Para decidir si un programa/algoritmo es eficiente hay que evaluarlo a nivel de su eficiencia
tiempo de ejecución y su eficiencia en espacios de almacenamiento necesarios. En la mayoría de
las veces se llega a una compensación entre ambos valores, como p. ej. almacenar resultados de
cálculos intermedios.

En la siguiente tabla podemos ver el comportamiento en tiempo de ejecución de diferentes


órdenes de complejidad en función de los datos a procesar:

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).

Para más información sobre complejidad y rendimiento en Python ver TimeComplexity

Compresiones de listas, conjuntos y diccionarios


La eliminación de bucles es un elemento esencial en la reducción del orden de complejidad
resultante de un algoritmo. Un blucle for cuyo cuerpo sólo contiene operaciones con tiempo
de ejecución constante (operaciones matemáticas, comparaciones, asignacios o accesos a
objetos en memoria) tiene un O(n), un blucle for que a su vez contiene otro bucle for
anidado tiene un O(n2 ) y si a éste último le añadimos un tercer bucle for anidado
obtendríamos un O(n3 ).
Las compresiones (comprehensions) de listas son una de las funciones de lenguaje de Python
más potentes. Permiten formar una nueva lista de forma concisa al filtrar los elementos de una
colección, transformando los elementos que pasan el filtro en una expresión concisa. Su sintaxis
básica es:

[expresion(valor) for valor in coleccion if condicion(valor)]


El código equivalente sería:

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.

cadenas = ['a', 'as', 'bat', 'car', 'dove', 'python']


[x.upper() for x in cadenas if len(x) > 2]

['BAT', 'CAR', 'DOVE', 'PYTHON']

Las comprensiones de conjuntos y diccionarios son una extensión natural, produciendo


conjuntos y diccionarios de una manera similar a la utilizada con las listas. Una comprensión para
un diccionario sería:

dict_comp = {key-expr:value-expr for value in coleccion if condicion}

{indice : valor for indice, valor in enumerate(cadenas)}

{0: 'a', 1: 'as', 2: 'bat', 3: 'car', 4: 'dove', 5: 'python'}

Para un conjunto sólo habría que sustituir las llaves por corchetes:

set_comp = {expr for value in coleccion if condicion}

{len(x) for x in cadenas}

{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)

CPU times: user 11.6 s, sys: 1.37 s, total: 13 s


Wall time: 13 s

%time b = [i^2 for i in range(100000000)]

CPU times: user 6.03 s, sys: 1.63 s, total: 7.66 s


Wall time: 7.66 s

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

['Emily', 'Michael', 'Steven', 'Javier']

[nombre for nombres in datos for nombre in nombres if nombre.upper().count('E') >=

['Emily', 'Michael', 'Steven', 'Javier']

lista_de_tuplas = [(1, 2, 3), (4, 5, 6), (7, 8, 9)]


[x for tupla in lista_de_tuplas for x in tupla]

[1, 2, 3, 4, 5, 6, 7, 8, 9]

# [list(tupla) for tupla in lista_de_tuplas]


[[x for x in tupla] for tupla in lista_de_tuplas]

[[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)

x=3, y=5 -> 8

resultado = sumatorio(123.45, 567.89)


print(resultado)

x=123.45, y=567.89 -> 691.34

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:

def sumatorio_ponderado(x, y, z=1):


print(f'{x=}, {y=}, {z=}', end=' -> ')
if z < 1:
return (x + y) * z
else:
return (x + y) / z

En la función anterior, x e y son parámetros posicionales, mientras que z es un parámetro


de tipo palabra clave.

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 '='.

La función anterior se puede llamar de cualquiera de estas maneras:

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)

x=3, y=5, z=1 -> 8.0


x=3, y=5, z=0.5 -> 4.0
x=3, y=5, z=2 -> 4.0

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:

resultado = sumatorio_ponderado(3, z=2, 5)


print(resultado)

File "<ipython-input-190-655bb12decc4>", line 1


resultado = sumatorio_ponderado(3, z=2, 5)
^
SyntaxError: positional argument follows keyword argument

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)

x=3, y=8, z=2 -> 5.5


x=3, y=8, z=2 -> 5.5

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

def sumatorio_ponderado(x, y, z=MULTIPLICADOR):


print(f'{x=}, {y=}, {z=}', end=' -> ')
if z < 1:
return (x + y) * z
else:
return (x + y) / z

resultado = sumatorio_ponderado(3, 5)
print(resultado)

MULTIPLICADOR = 2
resultado = sumatorio_ponderado(3, 5)
print(resultado)

x=3, y=5, z=1 -> 8.0


x=3, y=5, z=1 -> 8.0

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 ( * ).

def sumatorio_ponderado(*valores, z=1):


print(f'{valores=}, {z=}', end=' -> ')
if z < 1:
return sum(valores) * z
else:
return sum(valores) / z

resultado = sumatorio_ponderado(3, 5, 9, 11)


print(resultado)
resultado = sumatorio_ponderado(3, 5, 9, 11, z=0.5)
print(resultado)

valores=(3, 5, 9, 11), z=1 -> 28.0


valores=(3, 5, 9, 11), z=0.5 -> 14.0

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:

def imprime_argumentos(*args, **kwargs):


print(f'{args=}, {kwargs=}')

imprime_argumentos(1, 2, 3, name='John', age=30)

args=(1, 2, 3), kwargs={'name': 'John', 'age': 30}

En muchas ocasiones se utiliza args como nombre de parámetro para argumentos


posicionales y kwargs como nombre de parámetro para argumentos de tipo palabra clave.
Puede ocurrir además, una situación inversa a la anterior. Es decir, que la función espere una
lista fija de parámetros, pero que éstos, en vez de estar disponibles de forma separada, se
encuentren contenidos en una lista o tupla. En este caso, el signo asterisco ( * ) deberá
preceder al nombre de la lista o tupla que es pasada como argumento durante la llamada a la
función:

argumentos = (3, 5, 9, 11)


resultado = sumatorio_ponderado(*argumentos)
print(resultado)

valores=(3, 5, 9, 11), z=1 -> 28.0

A partir de Python 3.0 se ofrece la posibilidad de obligar a que determinados parámetros de la


función sean pasados sólo por nombre. Para ello, en la definición de los parámetros de la
función, tendremos que incluir un parámetro especial * que delimitará el tipo de parámetros.
Así, todos los parámetros a la derecha del separador estarán obligados a ser pasados como
pares clave=valor :

def sumatorio_ponderado(x, y, *, z=1):


print(f'{x=}, {y=}, {z=}', end=' -> ')
if z < 1:
return (x + y) * z
else:
return (x + y) / z

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)

x=3, y=5, z=1 -> 8.0


x=3, y=5, z=2 -> 4.0
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
<ipython-input-199-989a978042f8> in <module>
3 resultado = sumatorio_ponderado(3, 5, z=2)
4 print(resultado)
----> 5 resultado = sumatorio_ponderado(3, 5, 0.5)
6 print(resultado)

TypeError: sumatorio_ponderado() takes 2 positional arguments but 3 were given

A partir de Python 3.8 se ofrece la posibilidad de obligar a que determinados parámetros de la


función sean pasados sólo por posición. Para ello, en la definición de los parámetros de la
función, tendremos que incluir un parámetro especial / que delimitará el tipo de parámetros.
Así, todos los parámetros a la izquierda del delimitador estarán obligados a ser posicionales:

def sumatorio_ponderado(x, y, /, z=1):


print(f'{x=}, {y=}, {z=}', end=' -> ')
if z < 1:
return (x + y) * z
else:
return (x + y) / z

resultado = sumatorio_ponderado(x=3, y=8, z=2)


print(resultado)
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
<ipython-input-201-f42a62e9b074> in <module>
----> 1 resultado = sumatorio_ponderado(x=3, y=8, z=2)
2 print(resultado)

TypeError: sumatorio_ponderado() got some positional-only arguments passed as keywo


rd arguments: 'x, y'

Si mezclamos las dos estrategias anteriores podemos forzar a que una función reciba
argumentos de un modo concreto.

def sumatorio_ponderado(x, y, /, *, z=1):


print(f'{x=}, {y=}, {z=}', end=' -> ')
if z < 1:
return (x + y) * z
else:
return (x + y) / z

resultado = sumatorio_ponderado(3, 8, z=2) # única posible llamada


print(resultado)

x=3, y=8, z=2 -> 5.5

Funciones como argumentos


Las funciones se pueden utilizar en cualquier contexto de nuestro programa. Son objetos que
pueden ser asignados a variables, usados en expresiones, devueltos como valores de retorno o
pasados como argumentos a otras funciones:

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:

def aplicar_operacion(funcion, valores):


for valor in valores:
print(f'{funcion.__name__}({valor}) -> {funcion(valor)}')
aplicar_operacion(doble, (1, 2, 3, 4))

doble(1) -> 2
doble(2) -> 4
doble(3) -> 6
doble(4) -> 8

aplicar_operacion(cuadrado, (1, 2, 3, 4))

cuadrado(1) -> 1
cuadrado(2) -> 4
cuadrado(3) -> 9
cuadrado(4) -> 16

Devolviendo múltiples valores


Una de las características más flexibles de las funciones en Python, es la capacidad de devolver
múltiples valores desde una función con una sintaxis simple. Lo que sucede es que la cláusula
return devuelve un objeto, pudiendo ser éste una tupla, una lista, un diccionario o cualquier
otra estructura compleja:

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

Para recuperar la información del docstring se utiliza la función help :

help(cuadrados)

Help on function cuadrados in module __main__:

cuadrados(*valores)
La función devuelve una lista con el cuadrado de
cada valor proporcionado en el parámetro 'valores'

# docstring de la función sin formatear


cuadrados.__doc__
" La función devuelve una lista con el cuadrado de \n cada valor proporcion
ado en el parámetro 'valores'\n "

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:

1. Una primera línea de descripción de la función.


2. A continuación especificamos las características de los parámetros (incluyendo sus tipos).
3. Por último, indicamos si la función retorna un valor y sus características.

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.

def sumatorio_ponderado(x: int, y: int, z: int = 1) -> int:


print(f'{x=}, {y=}, {z=}', end=' -> ')
if z < 1:
return (x + y) * z
else:
return (x + y) / z

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 -> .

La anotación de tipos no es una declaración de tipos, simplemente tiene caracter informativo.


Hay herramientas como mypy que permiten hacer una comprobación de tipos.

sumatorio_ponderado(4.4, 5.6, z=0.5)

x=4.4, y=5.6, z=0.5 ->


5.0

La anotación de tipos permite definir tipos compuestos o indicar múltiples tipos:

def cuadrados(*valores: int | float) -> list[int|float]:


''' 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
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
<ipython-input-216-58398c0fc7e5> in <module>
----> 1 def cuadrados(*valores: int | float) -> list[int|float]:
2 ''' La función devuelve una lista con el cuadrado de
3 cada valor proporcionado en el parámetro 'valores'
4 '''
5 cuadrados = []

TypeError: unsupported operand type(s) for |: 'type' and 'type'

Alcance y ámbito de las variables


Las funciones pueden acceder a las variables en dos ámbitos diferentes: global y local. Todas las
variables que se asignan dentro de una función por defecto se asignan al espacio de nombres
local. El espacio de nombres local se crea cuando se llama a la función y se rellena
inmediatamente con los argumentos de la función. Una vez finalizada la función, el espacio de
nombres local se destruye (con algunas excepciones que están fuera del alcance de este
capítulo).

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)

En este caso después de la ejecución de funcion() , la variable a valdría [1, 2, 3] .

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)

En este caso después de la ejecución de funcion() , la variable a valdría [1, 2, 3, 4,


5, 6] .

El uso de global no se considera una buena práctica ya que puede inducir a confusión y tener
efectos colaterales indeseados.

Mutación, Alias y Copia


Cuando realizamos modificaciones a los argumentos de una función es importante tener en
cuenta si son mutables (listas, diccionarios, conjuntos, ...) o inmutables (tuplas, enteros,
flotantes, cadenas de texto, ...) ya que podríamos obtener efectos colaterales no deseados.

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:

def eliminar_duplicados_erronea(lista1, lista2):


for elemento in lista1:
if elemento in lista2:
lista1.remove(elemento)

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:

def eliminar_duplicados(lista1, lista2):


lista_auxiliar = lista1[:]
for elemento in lista_auxiliar:
if elemento in lista2:
lista1.remove(elemento)
l1 = [1, 2, 3, 4]
l2 = [1, 2, 5, 6]

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:

def apilar_elementos_erronea(elemento, resultado=[]):


resultado.append(elemento)
print(f'{resultado=}')

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]

Como el valor por defecto se establece cuando se define la función y no en la llamada, en la


segunda llamada el parámetro resultado tiene la información de la primera ejecución. Una
forma de resolver el problema es la siguiente codificación:

def apilar_elementos(elemento, resultado=None):


if resultado is None:
resultado = []
resultado.append(elemento)
print(f'{resultado=}')

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]

Funciones anónimas (funciones Lambda)


Python es compatible con las llamadas funciones anónimas o lambda, que son una forma de
escribir funciones que consisten en una sola declaración, cuyo resultado es el valor de retorno.
Se definen con la palabra clave lambda , que no tiene otro significado que "estamos declarando
una función anónima":

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.

# lista de cadenas ordenada en base al número distinto de letras


cadenas = ['foo', 'card', 'bar', 'aaaa', 'abab']
cadenas.sort(key=lambda x: len(set(list(x))))
cadenas

['aaaa', 'foo', 'abab', 'bar', 'card']

Programación funcional: map , filter y reduce


Estas funciones encajan perfectamente en el paradigna funcional, además son ampliamente
utilizadas para trabajar con secuencias y suelen aparecer vinculadas a funciones lambda.

La función map permite aplicar una función a cada uno de los elementos que componen un
iterable:

map(funcion, iterable)

# duplicamos los valores


map(lambda x: 2*x, (1, 2, 3, 4, 5, 6))

<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.

list(map(lambda x: 2*x, (1, 2, 3, 4, 5, 6)))

[2, 4, 6, 8, 10, 12]

El resulltado es equivalente a utilizar una lista por compresión:

[2*x for x in (1, 2, 3, 4, 5, 6)]

[2, 4, 6, 8, 10, 12]

La función filter selecciona aquellos elementos de un iterable que cumplan una determinada
condición.:

filter(funcion, iterable)

# seleccionamos los valores pares


filter(lambda x: x%2 == 0, (1, 2, 3, 4, 5, 6, 7, 8, 9))

<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.

list(filter(lambda x: x%2 == 0, (1, 2, 3, 4, 5, 6, 7, 8, 9)))

[2, 4, 6, 8]

De nuevo podemos obtener el mismo resultado haciendo uso de una lista por compresión:

[x for x in (1, 2, 3, 4, 5, 6, 7, 8, 9) if x%2 == 0]

[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.

from functools import reduce

reduce(lambda x, y: x * y, range(1, 6)) # ((((1 * 2) * 3) * 4) * 5)

120

cadenas = ['En', 'un', 'lugar', 'de', 'la', 'Mancha', 'de', 'cuyo', 'nombre', '...'
reduce(lambda x, y: x + ' ' + y, cadenas)

'En un lugar de la Mancha de cuyo nombre ...'

Llamadas de retorno (callback)


Para poder hacer llamadas a funciones de manera dinámica, es decir, desconociendo el nombre
de la función a la que se deseará llamar (por ejemplo funciones pasadas por parámetro a otras
funciones), Python dispone de dos funciones nativas: locals() y globals() . Ambas
funciones, retornan un diccionario. En el caso de locals() , éste diccionario se compone de
todos los elementos de ámbito local, mientras que el de globals() , retorna lo propio pero a
nivel global. Estas funciones tienen una gran variedad de usos y son especialmente útiles en
sincronización de procesos.

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.

La construcción de un iterador no es trivial. Por ejemplo, la implementación de cada objeto


iterador debe consistir en un método __iter__() y __next__() . Además del requisito
previo anterior, la implementación también debe tener una forma de rastrear el estado interno
del objeto y generar una excepción StopIteration una vez que no se puedan devolver más
valores. Estas reglas se conocen como el protocolo iterador.

# iterador que genera los cuadrados de un conjunto de valores


class Squares(object):
def __init__(self, start, stop):
self.start = start
self.stop = stop

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

for i in Squares(3, 9):


print(i, end=", ")

9, 16, 25, 36, 49, 64,

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:

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


iterator_un_diccionario = iter(un_diccionario)
iterator_un_diccionario

<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))

['a', 'b', 'c']

No se recomienda realizar esta acción, especialmente cuando el número de elementos que


devuelve el iterador es grande, ya que llevará mucho tiempo procesarlos.

Como la implementación de un iterador es compleja, una alternativa más simple es usar un


objeto generador. Un generador, en su forma más simple, es una función que devuelve
elementos de uno en uno en lugar de devolver una colección de elementos. La ventaja más
importante es que requieren muy poca memoria y no necesitan tener un tamaño predefinido.

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.

Los generadores no almacenan su contenido en la memoria como se esperaría que hiciera un


iterable típico. Por ejemplo, si el objetivo fuera encontrar todos los divisores de un entero
positivo, normalmente implementaríamos una función tradicional de la siguiente manera:

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))

[1, 2, 3, 5, 6, 10, 15, 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))

<generator object divisores at 0x7f1ef2bfa510>

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:

generador_cuadrados = (x ** 2 for x in range(1,11))


print(next(generador_cuadrados))
print(next(generador_cuadrados))
for x in generador_cuadrados:
print(x, end=" ")

1
4
9 16 25 36 49 64 81 100

### Generador de series de Fibonacci


def fibonacci_series(n):
a, b = 0, 1
for i in range(n):
yield a
a, b = b, a + b
for number in fibonacci_series(10):
print(number, end=", ")

0, 1, 1, 2, 3, 5, 8, 13, 21, 34,

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 contras más importantes:

• 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)

-> El generador recibe 4


El generador devuelve 4^1: 4
-> El generador recibe 3
El generador devuelve 3^3: 27
-> El generador recibe 2
El generador devuelve 2^5: 32

Funciones anidadas: internas y cierres


Se pueden declarar y utilizar funciones dentro de funciones, es lo que se denomina funciones
internas. Su ámbito está acotado a la función contenedora por lo que no son muy reutilizables:

SERVIDORES = ['gmail.com', 'outlook.es', 'icloud.com', 'yahoo.es']


def validar_email(email):
def validar_servidor(servidor):
return servidor in SERVIDORES

return '@' in email and validar_servidor(email.split('@')[1])


validar_email('[email protected]')

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

def sumar(x, y):


return x + y

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

los decoradores pueden actuar sobre los argumentos pasados a la función:

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.

Módulos, Paquetes y Espacios de Nombres


Los módulos y paquetes de Python, son dos mecanismos que facilitan la programación modular.

La programación modular se refiere al proceso de dividir una tarea de programación grande y


difícil de manejar en subtareas o módulos separados, más pequeños y más manejables. Luego,
los módulos individuales se pueden improvisar como bloques de construcción para crear una
aplicación más grande.

Existen varias ventajas al modularizar el código en una aplicación grande:

• Simplicidad: en lugar de centrarse en todo el problema en cuestión, un módulo normalmente


se centra en una parte relativamente pequeña del problema. Si está trabajando en un solo
módulo, tendrá un dominio de problema más pequeño para entender. Esto hace que el
desarrollo sea más fácil y menos propenso a errores.
• Mantenibilidad: los módulos generalmente están diseñados para imponer límites lógicos
entre diferentes dominios de problemas. Si los módulos se escriben de una manera que
minimice la interdependencia, existe una menor probabilidad de que las modificaciones a un
solo módulo tengan un impacto en otras partes del programa. (Es posible que incluso pueda
realizar cambios en un módulo sin tener ningún conocimiento de la aplicación fuera de ese
módulo). Esto hace que sea más viable para un equipo de muchos programadores trabajar
en colaboración en una aplicación grande.
• Reutilizabilidad: la funcionalidad definida en un único módulo puede ser reutilizada
fácilmente (a través de una interfaz definida apropiadamente) por otras partes de la
aplicación. Esto elimina la necesidad de duplicar el código.
• Alcance: los módulos normalmente definen un espacio de nombres separado, lo que ayuda a
evitar colisiones entre identificadores en diferentes áreas de un programa. (Uno de los
principios del Zen de Python es que los espacios de nombres son una gran idea: ¡hagamos
más de eso!)

Las funciones, módulos y paquetes son construcciones en Python que promueven la


modularización del código. Tanto los módulos como los paquetes organizan y estructuran el
código, pero tienen diferentes propósitos. En términos sencillos, un módulo es un único archivo
que contiene código Python, mientras que un paquete es una colección de módulos organizados
en una jerarquía de directorios.

En Python, un módulo es un archivo único .py que contiene definiciones y declaraciones


de Python. Estas definiciones y declaraciones pueden incluir variables, funciones y clases y
pueden usarse para organizar funciones relacionadas en un paquete único y reutilizable. El
módulo organiza y reutiliza código en Python agrupando el código relacionado en un solo
archivo.

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 .

Los paquetes de Python son colecciones de módulos que proporcionan un conjunto de


funcionalidades relacionadas y estos módulos están organizados en una jerarquía de
directorios. En términos simples, los paquetes en Python son una forma de organizar módulos
relacionados en un único espacio de nombres.

• Los paquetes en Python se instalan mediante un administrador de paquetes como pip .


• Un paquete puede tener múltiples subpaquetes y módulos, y cada módulo y subpaquete
tiene su propio espacio de nombres.
• Cada paquete de Python debe contener un archivo llamado __init__.py . En el caso más
simple, __init__.py puede ser un archivo vacío, pero también puede ejecutar el código
de inicialización del paquete.

Algunos paquetes populares en Python son Scipy , Numpy , Pandas , Matplotlib y


Sckit-Lern .

Un espacio de nombres en Python es una asignación de nombres a objetos. Es una forma


de organizar los símbolos de un programa poniéndolos en diferentes contextos. Cada
módulo en Python tiene su propia tabla de símbolos privada, que se utiliza para almacenar los
nombres definidos en ese módulo. Esta tabla se llama espacio de nombres del módulo. Cuando
se importa un módulo, todos los símbolos en la tabla de símbolos del módulo se ponen a
disposición del programa que llama, pero se colocan en el espacio de nombres del módulo, en
lugar del del programa que llama. De esta manera, no es posible que el programa que llama
sobrescriba accidentalmente un símbolo definido en el módulo.
En Python, existen varios tipos de espacios de nombres que se pueden utilizar para organizar
símbolos:

• Espacio de nombres global (Global namespace): este es el espacio de nombres que


contiene todos los símbolos que se definen en el nivel superior de un módulo o script.
• Espacio de nombres local (Local namespace: este es el espacio de nombres que se crea
cada vez que define una función o método. Contiene todos los símbolos que se definen
dentro de la función o método.
• Espacio de nombres del módulo (Module namespace): este es el espacio de nombres que
contiene todos los símbolos que se definen en un módulo.
• Espacio de nombres integrado (Built-in namespace): este es un espacio de nombres
predefinido que contiene todos los símbolos integrados que están disponibles en Python,
como int , str , list , etc.

Hay un concepto más en la estructuración del código, la librería, que es un conjunto de


módulos y paquetes que se distribuyen conjuntamente.

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:

• El directorio desde el que se ejecutó el script de entrada o el directorio actual si el intérprete


se ejecuta de forma interactiva.
• La lista de directorios contenidos en la variable de entorno PYTHONPATH , si está
configurada. (El formato de esta variable depende del sistema operativo, pero debe imitar la
variable de entorno PATH ).
• lista de directorios dependientes de la instalación configurados en el momento de instalar
Python

Se puede acceder a la ruta de búsqueda resultante en la variable de Python sys.path , que se


define en el módulo sys :

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:

• Colocar el módulo en el directorio donde se encuentra el script de entrada o en el directorio


actual, si es interactivo
• Modificar la variable de entorno PYTHONPATH para que contenga el directorio donde se
encuentra el módulo de iniciar el intérprete.
• Colocar el módulo en uno de los directorios que ya están contenidos en la variable
PYTHONPATH .
• Colocar el archivo del módulo en cualquier directorio y modificar sys.path en tiempo de
ejecución para que contenga ese directorio.

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 :

sound Top-level package


│ __init__.py Initialize the sound package
├── formats Subpackage for file format conversions
│ │ __init__.py
│ ├── wavread.py
│ ├── wavwrite.py
│ ├── aiffread.py
│ ├── aiffwrite.py
│ ├── auread.py
│ ├── auwrite.py
│ └── ...
├── effects Subpackage for sound effects
│ │ __init__.py
│ ├── echo.py
│ ├── surround.py
│ ├── reverse.py
│ └── ...
└── filters Subpackage for filters
│ __init__.py
├── equalizer.py
├── vocoder.py
├── karaoke.py
└── ...

Importar módulo completo

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 :

sound.effects.echo.echofilter(entrada, salida, retardo=0.7, atten=4)


Cuando se usa esta sintaxis, cada elemento, excepto el último, debe ser un paquete; el último
elemento puede ser un módulo o un paquete, pero no puede ser una clase o función o variable
definida en el elemento anterior.
from <package> import <module_name>

Una forma alternativa de importar el submódulo es:

from sound.effects import echo


Esto también carga el submódulo echo , y lo hace disponible sin su prefijo de paquete, por lo
que se puede usar de la siguiente manera:

echo.echofilter(entrada, salida, retardo=0.7, atten=4)


Cuando se utiliza esta sintaxis el elemento puede ser un submódulo (o subpaquete) del paquete,
o algún otro nombre definido en el paquete, como una función, clase o variable. La declaración
de importación primero prueba si el componente está definido en el paquete. Si no, asume que
es un módulo e intenta cargarlo.

Importar partes de un módulo

from <module_name> import <name(s)>

Otra variación más es importar la función o variable deseada directamente:

from sonido.effects.echo import echofilter


De nuevo, esto carga el submódulo de eco, pero hace que su función echofilter esté
disponible directamente:

echofilter(entrada, salida, retardo=0.7, atten=4)


Es posible también, importar más de un elemento en la misma instrucción. Para ello, cada
elemento irá separado por una coma (,) y un espacio en blanco:

from sound.effects import echo, surround


Incluso es posible importar indiscriminadamente todo lo que hay desde un módulo de una sola
vez:

from sonido.effects.echo import *


Idealmente, uno esperaría que esta importación solicite al sistema de archivos que encuentre
qué submódulos están presentes en el paquete y los importe todos. Esto puede llevar mucho
tiempo y la importación de submódulos puede tener efectos secundarios no deseados que solo
deberían ocurrir cuando el submódulo se importa explícitamente. Esta sintaxis se considera una
mala práctica en el código de producción.

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.

Importar usando un alias

import <module_name> as <alt_name>

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:

import sound.effects.echo as eco


Esto carga el submódulo sound.effects.echo , pero sus componentes deben ser
referenciados con el alias definido.
eco.echofilter(entrada, salida, retardo=0.7, atten=4)
También es posible indicar un alias para las funciones u objetos importados:

from <module_name> import <name> as <alt_name>

from sonido.effects.echo import echofilter as ef


De nuevo, esto carga el submódulo de eco, pero hace que su función echofilter esté
disponible bajo el alias ef :

ef(entrada, salida, retardo=0.7, atten=4)

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 :

from ejercicios.fact import fact

fact(7)

5040

Como ejecutarse directamente desde la consola de sistema pasándole como argumento el


número del que se quiere calcular el factorial.

!python ./ejercicios/fact.py 7

5040

%run ./ejercicios/fact.py 7

5040

Librerías en Data Science


Hay varias librerías de Python que son fundamentales en Data Science. Echemos un vistazo
rápido al tipo de funcionalidad que ofrecen:

• 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:

print 'Hola Mundo...'

File "<ipython-input-273-344ef21b7221>", line 1


print 'Hola Mundo...'
^
SyntaxError: Missing parentheses in call to 'print'. Did you mean print('Hola Mund
o...')?

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.

Incluso si una declaración o expresión es sintácticamente correcta, puede causar un error


cuando se intenta ejecutarla (accesos fuera de rango a listas o tuplas, accesos a claves
inexistentes en diccionarios, etc.). Los errores detectados durante la ejecución se llaman
excepciones y no son incondicionalmente fatales. Podemos entender las excepciones como
bloques de código que se lanzan cuando se produce un error en la ejecución de un programa. El
manejo adecuado de los errores o excepciones de Python es una parte importante de la creación
de programas sólidos.

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

TypeError Operación sobre un objeto de tipo inapropiado

ValueError Operción sobre un objeto de tipo correcto pero de valor inapropiado

AttributeError Referencia a un atributo inexistente

IndexError Índice de una secuencia fuera de rango

KeyError Calve de diccionario no encontrada

ZeroDivisionError División o módulo de división por 0

ImportError Error al importar un módulo

RuntimeError Error de ejecución genérico

El resto de excepciones integradas está en excepciones incorporadas

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')

ValueError: could not convert string to float: 'hola'

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 .

2. Si no ocurre ninguna excepción, la cláusula except se omite y la ejecución de la cláusula


try finaliza.

3. Si ocurre una excepción durante la ejecución de la cláusula try , se omite el resto de la


cláusula a partir de la instrucción que genera la excepción.

• Si el tipo de la excepción que ha ocurrido coincide con la excepción nombrada después de la


palabra clave except , se ejecuta la cláusula except y una vez finaliza, continúa la
ejecución con el resto de instrucciones después del bloque try/except .
• Si ocurre una excepción que no coincide con la indicada en la cláusula except se pasa a
los procesos llamadores; si no se encuentra un gestor, se genera una unhandled
exception (excepción no gestionada) y la ejecución se interrumpe con un mensaje.

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...

Los gestores de excepciones no sólo gestionan excepciones que ocurren inmediatamente en la


cláusula try, sino también aquellas que ocurren dentro de funciones que son llamadas (incluso
indirectamente) en la cláusula try. Por ejemplo:

try:
f1('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...

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...')

Valor proporcionado no válido [could not convert string to float: 'hola']

...resto de instrucciones...

f1('hola')
print('\n...resto de instrucciones...')

Valor proporcionado no válido [could not convert string to float: 'hola']

...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...')

zde.args=("could not convert string to float: 'hola'",)


Valor proporcionado x='hola' no es válido -> could not convert string to float: 'ho
la'

...resto de instrucciones...

Siempre es posible volcar la pila seguimiento de la excepción (Traceback):


def f3(x):
try:
return float(x)
except ValueError as zde:
import traceback
traceback.print_exc()

f3('hola')

Traceback (most recent call last):


File "<ipython-input-281-963e16ef971d>", line 3, in f3
return float(x)
ValueError: could not convert string to float: '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 la cláusula finally ejecuta una declaración break , continue o return , las


excepciones no se vuelven a lanzar.

• Si el bloque try llega a una sentencia break , continue o return , la cláusula


finally se ejecutará justo antes de la ejecución de dicha sentencia.

• 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> # equivalente a 'raise <Excepcion>()'


También es posible pasar 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')

TypeError: El valor proporcionado 3.8 no es entero

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

ValueError: El valor proporcionado 3 no es par

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}')

ZeroDivisionError: float division by zero

Si se produce una excepción no gestionada dentro de una sección except , se le adjuntará la


excepción que se está gestionando y se incluirá en el mensaje de error. Para indicar que una
excepción es consecuencia directa de otra, la sentencia raise permite una cláusula opcional
from :

raise <excepción> from <excepción origen>


También permite deshabilitar el encadenamiento automático de excepciones utilizando el
modismo from None :

raise <excepción> from None

Excepciones definidas por el usuario


Los programas pueden definir sus propias excepciones creando una nueva clase excepción (ver
POO en Python). Las excepciones, típicamente, deberán derivar de la clase Exception ,
directa o indirectamente.

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

EnteroParError: El valor proporcionado 3 no es par

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)

EnteroParError: El valor proporcionado 2.0 no es entero

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.

# f1() llama a f2() y este a su vez a f3()


def f3(x):
return float(x)

%xmode Plain
f1('hola')
Exception reporting mode: Plain
Traceback (most recent call last):

File "<ipython-input-294-714c6aede731>", line 2, in <module>


f1('hola')

File "<ipython-input-274-51f51e8ee670>", line 2, in f1


return f2(x)

File "<ipython-input-274-51f51e8ee670>", line 5, in f2


return f3(x)

File "<ipython-input-293-e49e2ea69218>", line 3, in f3


return float(x)

ValueError: could not convert string to float: 'hola'

%xmode Verbose
f1('hola')

Exception reporting mode: Verbose


---------------------------------------------------------------------------
ValueError Traceback (most recent call last)
<ipython-input-295-1c05aca0a4ea> in <module>
1 get_ipython().run_line_magic('xmode', 'Verbose')
----> 2 f1('hola')
global f1 = <function f1 at 0x7f1ef2c01700>

<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'

ValueError: could not convert string to float: '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.

open(ruta, [[modo="rt"], encoding="none"])


De forma predeterminada, los archivos se abre en modo de solo lectura y texto 'rt' .

La siguiente tabla muestra los diferentes modos de trabajo de los ficheros en Python:

Modo Descripción

r Solo lectura

w Solo escritura. Sobreescribe el archivo si existe. Crea el archivo si no existe.

x solo escritura. Falla si existe el archivo.

a Añade a un fichero existente. Crea el archivo si no existe.

r+ Lectura y escritura

b Modo binario (i.e., rb )

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

write(str) Escribe la cadena pasada al archivo

writelines(strings) Escribe las cadenas pasada al archivo

close() Cierra el fichero

flush() Vaciar el búfer de E/S interno en el disco

seek(pos) Mueve el apuntador del fichero a la posición indicada por pos (entero)

tell() Devuelve la posición actual del apuntador del fichero

closed True si el fichero está cerrado

mode Devuelve el modo de operación del fichero

encoding Devuelve la codificación de caracteres utilizada por el fichero

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

<_io.TextIOWrapper name='./data/teoria/segismundo.txt' mode='r' encoding='UTF-8'>

Las funciones read y readlines sino se proporciona el argumento opcional size leen
todo el contenido del fichero de una sola vez.

f.read()

'Sueña el rico en su riqueza,\nque más cuidados le ofrece;\n\nsueña el pobre que p


adece\nsu miseria y su pobreza;\n\nsueña el que a medrar empieza,\nsueña el que af
ana y pretende,\nsueña el que agravia y ofende,\n\ny en el mundo, en conclusión,\n
todos sueñan lo que son,\naunque ninguno lo entiende.\n\n'

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:

Es importante cerrar explícitamente los archivos cuando haya terminado de procesarlos,


llamado al método close. Al cerrar un archivo se liberan sus recursos en el sistema operativo.

f = open(path)
for linea in f:
print(linea.rstrip()) # strip elimina el caracter de fin-de-linea (EOL)
f.close()

Sueña el rico en su riqueza,


que más cuidados le ofrece;

sueña el pobre que padece


su miseria y su pobreza;

sueña el que a medrar empieza,


sueña el que afana y pretende,
sueña el que agravia y ofende,

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;

sueña el pobre que padece


su miseria y su pobreza;

sueña el que a medrar empieza,


sueña el que afana y pretende,
sueña el que agravia y ofende,

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

['Sueña el rico en su riqueza,',


'que más cuidados le ofrece;',
'',
'sueña el pobre que padece',
'su miseria y su pobreza;',
'',
'sueña el que a medrar empieza,',
'sueña el que afana y pretende,',
'sueña el que agravia y ofende,',
'',
'y en el mundo, en conclusión,',
'todos sueñan lo que son,',
'aunque ninguno lo entiende.',
'']

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)

Sueña el rico en su riqueza,


que más cuidados le ofrece;

sueña el pobre que padece


su miseria y su pobreza;

sueña el que a medrar empieza,


sueña el que afana y pretende,
sueña el que agravia y ofende,

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.

# el fichero 'segismundo.txt' contiene caracteres no-ASCII con codificación UTF-8


# UTF-8 es una codificación Unicode de longitud variable, así que cuando se solicita un númer
# del archivo, Python lee suficientes bytes del archivo para decodificar esa cantidad de cara
with open(path) as f:
caracteres = f.read(10)
caracteres

'Sueña el r'

# Si abrimos el archivo en modo 'rb' en cambio,


# se leen únicamente el número de bytes solicitados
with open(path, 'rb') as f:
caracteres = f.read(10)
caracteres

b'Sue\xc3\xb1a el '

# Se puede utilizar el método `decode` de las cadenas de texto para


# decodificar la información siempre que esta esté correctamente formada
caracteres.decode('utf8')

'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

sys.getdefaultencoding() Retorna la codificación de caracteres por defecto

Retorna la codificación de caracteres que se utiliza para convertir los


sys.getfilesystemencoding()
nombres de archivos unicode en nombres de archivos del sistema

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

<_io.TextIOWrapper name='./data/teoria/salida.txt' mode='w' encoding='UTF-8'>

Siempre que se abre un fichero en modo escritura utilizando el argumento w, el fichero se


inicializa, borrando cualquier contenido que pudiera tener. Hay que asegurarse que la ruta
hasta ese fichero (las carpetas) existen. En otro caso obtenemos un error de tipo
FileNotFoundError.

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)

Especialmente en el caso de la escritura de ficheros, se recomienda encarecidamente cerrar los


ficheros para evitar pérdida de datos.

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

os.access(path, modo_de_acceso) Saber si se puede acceder a un archivo o directorio

os.getcwd() Conocer el directorio actual

os.chdir(nuevo_path) Cambiar de directorio de trabajo

os.chroot() Cambiar al directorio de trabajo raíz

os.chmod(path, permisos) Cambiar los permisos de un archivo o directorio

os.chown(path, permisos) Cambiar el propietario de un archivo o directorio

os.mkdir(path[, modo]) Crear un directorio

os.mkdirs(path[, modo]) Crear directorios recursivamente

os.remove(path) Eliminar un archivo

os.rmdir(path) Eliminar un directorio

os.removedirs(path) Eliminar directorios recursivamente

os.rename(actual, nuevo) Renombrar un archivo

os.symlink(path, nombre_destino) Crear un enlace simbólico


El módulo os también nos provee de un diccionario con las variables de entorno relativas al
sistema. Se trata del diccionario environ :

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

os.path.abspath(path) Ruta absoluta

os.path.basename(path) Directorio base

os.path.exists(path) Saber si un directorio existe

os.path.getatime(path) Conocer último acceso a un directorio

os.path.getsize(path) Conocer tamaño del directorio

os.path.isabs(path) Saber si una ruta es absoluta

os.path.isfile(path) Saber si una ruta es un archivo

os.path.isdir(path) Saber si una ruta es un directorio

os.path.islink(path) Saber si una ruta es un enlace simbólico

os.path.ismount(path) Saber si una ruta es un punto de montaje

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.

La siguientes tabla muestra algunas sintaxis de expresiones regulares de Python.

Patrón Descripción

^ Coincide con el comienzo de la línea

$ Coincide con el final de la línea

. Coincide con cualquier carácter individual excepto una nueva línea

[...] Coincide con cualquier carácter individual que esté entre los corchetes

[^...] Coincide con cualquier carácter individual que no esté entre los corchetes

re* Coincide con cero o más apariciones de la expresión anterior


Patrón Descripción

re+ Coincide con una o más ocurrencias de la expresión anterior

re? Coincide con cero o una aparición de la expresión anterior

re{n} Coincide exactamente con n número de apariciones de la expresión anterior

re{n,} Coincide con no más apariciones de la expresión anterior

re{n,m} Coincide con al menos n y como máximo m apariciones del expresión precedente

a|b Coincide con a o b

(re) Agrupa expresiones regulares y recuerda el texto coincidente

(?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 dentro de una expresión regular

(?: re) Agrupa expresiones regulares sin recordar el texto coincidente

(?imx: re) Alterna temporalmente las opciones i, m o x entre paréntesis

(?-imx:
Desactiva temporalmente las opciones i, m o x entre paréntesis.
re)

(?#...) Comentario.

(?= re) Especifica la posición mediante un patrón. no tiene rango.

(?! re) Especifica la posición usando la negación del patrón. no tiene rango.

(?> re) Coincide con el patrón independiente sin retroceso

\w Coincide con caracteres de palabra

\W Coincide con caracteres que no son palabras

\s Coincide con los espacios en blanco. equivalente a [\ t \ n \ r \ f]

\S Coincide con espacios que no son en blanco

\d Coincide con dígitos. equivalente a [0-9]

\D Coincide con no dígitos

\A Coincide con el comienzo de la cadena

Coincide con el final de la cadena, si existe una nueva línea, coincide justo antes de la
\Z
nueva línea

\z Coincide con el final de la cadena

\G Los partidos apuntan donde terminó el último partido

\b Coincide con los límites de las palabras cuando están fuera de los corchetes

\B Coincide con los límites que no son de palabras

\n, \t, etc. Coincide con nuevas líneas, retornos de carro, tabulaciones, etc.

\1...\9 Coincide con la enésima subexpresión agrupada

\10 Coincide con la enésima subexpresión agrupada si ya coincidió

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"""

• re.search(patrón, cadena, flags=0). Busca en la «cadena» el primer lugar donde el


«patrón» de la expresión regular produce una coincidencia, y retorna un objeto match
correspondiente. Retorna None si ninguna posición en la cadena coincide con el patrón.
• re.match(patrón, cadena, flags=0). Si cero o más caracteres al principio de la «cadena»
coinciden con el«patrón» de la expresión regular, retorna un objeto match
correspondiente. Retorna None si la cadena no coincide con el patrón. Incluso en modo
MULTILINE sólo coincidirá al principio de la cadena y no al principio de cada línea.
• re.fullmatch(patrón, cadena, flags=0). Si toda la «cadena» coincide con el «patrón» de la
expresión regular, retorna un correspondiente objeto match . Retorna None si la cadena
no coincide con el patrón.

course_match = re.search('[0-9]+', CoursesData)


if course_match:
print(course_match)
# Retorna la primera de la coincidencia.
# equivale a course_match[0] y course_match.group(0)
print(f'resultado: {course_match.group()}')
print(f'span: {course_match.span()}')
print(f'span start: {course_match.start()}')
print(f'span end: {course_match.end()}')

<re.Match object; span=(0, 3), match='101'>


resultado: 101
span: (0, 3)
span start: 0
span end: 3

course_match = re.search(r'(\d{1})(\d{2})', CoursesData)


print(f'resultado: {course_match.groups()}')
for grupo_id in range(len(course_match.groups())+1):
print(f' - resultado {grupo_id}: {course_match.group(grupo_id)}')
print(f' - span {grupo_id}: {course_match.span(grupo_id)}')

resultado: ('1', '01')


- resultado 0: 101
- span 0: (0, 3)
- resultado 1: 1
- span 1: (0, 1)
- resultado 2: 01
- span 2: (1, 3)

• re.findall(patrón, cadena, flags=0). Retorna todas las coincidencias no superpuestas del


«patrón» en la «cadena», como una lista de strings o tuplas. La «cadena» se escanea de
izquierda a derecha y las coincidencias se retornan en el orden en que se encuentran.

# Extrae todos los números de curso


course_numbers = re.findall('[0-9]+', CoursesData)
print (course_numbers)
# Extrae todos los códigos de curso
course_codes = re.findall('[A-Z]{3}', CoursesData)
print (course_codes)
# Extrae todos los nombres de curso
course_names = re.findall('[A-Za-z]{4,}', CoursesData)
print (course_names)
course_names = re.findall('[a-z]{4,}', CoursesData, re.IGNORECASE)
print (course_names)

['101', '205', '189']


['COM', 'MAT', 'ENG']
['Computers', 'Mathematics', 'English']
['Computers', 'Mathematics', '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:

# Extrae todos los componentes de todos los cursos


course_pattern = r'([0-9]+)\s*([A-Z]{3})\s*([A-Za-z]{4,})'
print(re.findall(course_pattern, CoursesData))
# Extrae todos los componentes que contienen letras
print(re.findall('[a-zA-Z]+', CoursesData))

[('101', 'COM', 'Computers'), ('205', 'MAT', 'Mathematics'), ('189', 'ENG', 'Englis


h')]
['COM', 'Computers', 'MAT', 'Mathematics', 'ENG', 'English']

• re.split(patrón, cadena, maxsplit=0, flags=0). Divide la «cadena» por el número de


ocurrencias del «patrón». Si se utilizan paréntesis de captura del «patrón», entonces el
texto de todos los grupos en el patrón también se retornan como parte de la lista resultante.

# Divide los componentes por los espaciones en blanco


print(re.split(r'\s+', CoursesData))

['101', 'COM', 'Computers', '205', 'MAT', 'Mathematics', '189', 'ENG', 'English']

• re.sub(patrón, reemplazo, cadena, count=0, flags=0). Retorna la cadena obtenida


reemplazando las ocurrencias no superpuestas del «patrón» en la «cadena» por el
«reemplazo». Si el patrón no se encuentra, se retorna la «cadena» sin cambios.
«reemplazo» puede ser una cadena o una función; si es una cadena, cualquier barra inversa
escapada en ella es procesada.

re.sub('([0-9]+)', r'N-\1', CoursesData)

'N-101 COM Computers\n N-205 MAT Mathematics\n N-189 ENG English'

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']

Una explicación en profundidad se puede encontrar en re — Operaciones con expresiones


regulares

Usando el intérprete de Python


El intérprete de Python generalmente se instala como /usr/local/bin/python3.7 en
aquellas máquinas donde está disponible; al poner /usr/local/bin en la ruta de búsqueda
de su shell de Unix es posible iniciarlo escribiendo el comando:

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 .

Si se escribe un carácter de final de archivo ( Control-D en Unix, Control-Z en Windows)


en el indicador primario, el intérprete sale con un estado de salida cero. Si eso no funciona,
puede salir del intérprete escribiendo el siguiente comando: quit() .

Scripts Python ejecutables


En los sistemas BSD Unix, los scripts de Python se pueden hacer directamente ejecutables,
como los scripts de shell, poniendo la línea:

#!/usr/bin/env python3.7

(asumiendo que el intérprete se encuentra en la RUTA del usuario) al comienzo de la secuencia


de comandos y le da al archivo un modo ejecutable. Los #! Deben ser los dos primeros
caracteres del archivo. En algunas plataformas, esta primera línea debe terminar con un final de
línea de estilo Unix ( '\n' ), no con un final de línea de Windows ( '\r\n' ). Tenga en cuenta
que el carácter hash o pound, '#' , se usa para iniciar un comentario en Python.

El script puede recibir un modo ejecutable, o permiso, mediante el comando chmod.

$ 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:

• Una potente shell interactiva.


• Un núcleo para Jupyter.
• Soporte para visualización de datos interactiva y uso de kits de herramientas GUI.
• Intérpretes flexibles e integrables para cargar en sus propios proyectos.
• Herramientas fáciles de usar y de alto rendimiento para computación paralela.

Comandos de shell en IPython


Se puede utilizar cualquier comando de la línea de comandos en IPython colocándolo con el
caracter " ! " como prefijo:

# !dir en Windows
!ls

cachedir docs joblib.ipynb python.ipynb


dask.ipynb ejercicios matplotlib.ipynb python_poo.ipynb
dask-worker-space fibonacci.ipynb mprun_demo.py
data images numpy.ipynb
data-pandas.ipynb intro-pandas.ipynb __pycache__

# !cd en Windows
!pwd

/workspace/Curso Python

Pasar valores hacia y desde el Shell


Los comandos de shell no solo pueden invocarse desde IPython, sino que también pueden
interactuar con el espacio de nombres de IPython.

listado = !ls *.ipynb


print(listado)

['dask.ipynb', 'data-pandas.ipynb', 'fibonacci.ipynb', 'intro-pandas.ipynb', 'jobli


b.ipynb', 'matplotlib.ipynb', 'numpy.ipynb', 'python.ipynb', 'python_poo.ipynb']

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.

La comunicación en la otra dirección, pasar variables de Python al shell, es posible a través de la


sintaxis {varname} :

patron = "*.ipynb"
!ls {patron}

dask.ipynb intro-pandas.ipynb numpy.ipynb


data-pandas.ipynb joblib.ipynb python.ipynb
fibonacci.ipynb matplotlib.ipynb python_poo.ipynb

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

dask.ipynb intro-pandas.ipynb* numpy.ipynb


data-pandas.ipynb* joblib.ipynb python.ipynb
fibonacci.ipynb matplotlib.ipynb* python_poo.ipynb

Los comando mágicos disponibles son:

%lsmagic

Available line magics:


%alias %alias_magic %autoawait %autocall %automagic %autosave %bookmark %ca
t %cd %clear %colors %conda %config %connect_info %cp %debug %dhist %dir
s %doctest_mode %ed %edit %env %gui %hist %history %killbgscripts %ldir
%less %lf %lk %ll %load %load_ext %loadpy %logoff %logon %logstart %logs
tate %logstop %lprun %ls %lsmagic %lx %macro %magic %man %matplotlib %me
mit %mkdir %more %mprun %mv %notebook %page %pastebin %pdb %pdef %pdoc
%pfile %pinfo %pinfo2 %pip %popd %pprint %precision %prun %psearch %psour
ce %pushd %pwd %pycat %pylab %qtconsole %quickref %recall %rehashx %reloa
d_ext %rep %rerun %reset %reset_selective %rm %rmdir %run %save %sc %set
_env %store %sx %system %tb %time %timeit %unalias %unload_ext %who %who
_ls %whos %xdel %xmode

Available cell magics:


%%! %%HTML %%SVG %%bash %%capture %%debug %%file %%html %%javascript %%js
%%latex %%markdown %%memit %%mprun %%perl %%prun %%pypy %%python %%python2
%%python3 %%ruby %%script %%sh %%svg %%sx %%system %%time %%timeit %%writ
efile

Automagic is ON, % prefix IS NOT needed for line magics.

Son especilamente interesantes algunos como:

• %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;

sueña el pobre que padece


su miseria y su pobreza;

sueña el que a medrar empieza,


sueña el que afana y pretende,
sueña el que agravia y ofende,
y en el mundo, en conclusión,
todos sueñan lo que son,
aunque ninguno lo entiende.

Recuperando valores desde la consola


Aunque lo habitual es proporciona los parámetro que necesita un programa como argumentos
desde la consola, también es posible solicitar al usuario que los proporcione desde el teclado (la
entrada estandar al sistema), para ello Python propociona la función input :

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 .

user = input("Introduce tu usuario: ")


print(f"Bienvenido, {user}")

Introduce tu usuario: Pepe


Bienvenido, Pepe

De forma predeterminada, la función input convierte la entrada en una cadena, aunque


escribamos un número. Si queremos operar con el valor devuelvo como su fuera un número
habrá que convertirlo adecuadamente:

pesetas = int(input("Dame una cantidad en pesetas: "))


print(f"{pesetas} pesetas son {round(pesetas / 166.386, 2)} euros")

Dame una cantidad en pesetas: 123


123 pesetas son 0.74 euros
Depuración
La herramienta estándar de Python para la depuración interactiva es pdb , el depurador de
Python. Este depurador permite al usuario recorrer el código línea por línea para ver qué podría
estar causando un error más difícil. La versión mejorada de IPython de esto es ipdb , el
depurador de IPython. En IPython, quizás la interfaz más conveniente para la depuración es el
comando mágico %debug . Si se llama después de hacer pulsar en una excepción, se abrirá
automáticamente un mensaje de depuración interactivo en el punto de la excepción. El prompt
ipdb le permite explorar el estado actual de la pila, explorar las variables disponibles e incluso
ejecutar comandos de Python. También podemos subir ( up ) y bajar ( down ) por la pila y
explorar los valores de las variables. La siguiente tabla muestra algunos de los comandos más
habituales:

Comando Descripción

list Muestra la ubicación actual en el archivo

h(elp) Muestra una lista de comandos o busca ayuda sobre un comando específico

q(uit) Salir del depurador y del programa

c(ontinue) Salir del depurador; continuar en el programa

n(ext) Ir al siguiente paso del programa

<enter> Repite el comando anterior

p(rint) Imprimir variables

s(tep) Paso a una subrutina

r(eturn) Salir de una subrutina

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))

CPU times: user 509 µs, sys: 0 ns, total: 509 µs


Wall time: 515 µs
49995000

%timeit sum(range(10000))

134 µs ± 10.1 µs per loop (mean ± std. dev. of 7 runs, 10000 loops each)

Haciendo uso de un único caracter % se ejecuta el comando mágico a nivel de línea,


haciendo uso de dos caracteres %% se ejecuta a nivel de celda (scripts multilínea).

%%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)

A veces, es interesante poder cronometrar los tiempos de las diferentes declaraciones de un


bloque de instrucciones en el contexto y no a nivel individual. Python contiene un generador de
perfiles de código incorporado, pero IPython ofrece una forma mucho más conveniente de usar
este generador de perfiles, en la forma de la función mágica %prun .

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:

1. Instalar el paquete line_profiler :

$ pip install line_profiler

2. Cargar la extensión line_profiler , que se ofrece como

parte de este paquete:

%load_ext line_profiler

3. Utilizar el comando mágico %lprun :

%load_ext line_profiler

%lprun -f sum_of_lists sum_of_lists(5000)

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:

1. Instalar el paquete memory_profiler :

$ pip install memory_profiler

2. Cargar la extensión memory_profiler , que se ofrece como parte de este paquete:

%load_ext memory_profiler

3. Utilizar el comando mágico %memit :

%load_ext memory_profiler

%memit sum_of_lists(1000000)

peak memory: 4024.26 MiB, increment: 75.50 MiB

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.

Esta situación se solventa creando un módulo python con la funcionalidad a evaluar:


%%file mprun_demo.py
def sum_of_lists(N):
total = 0
for i in range(5):
L = [j^(j>>i) for j in range(N)]
total += sum(L)
del L # remove reference to L
return total

Overwriting mprun_demo.py

Y posteriormente importar la función y ejecutar el comando mágico:

from mprun_demo import sum_of_lists


%mprun -f sum_of_lists sum_of_lists(100000)

Este gasto de memoria hay que añadirlo al que utiliza el propio intérprete de Python.

También podría gustarte