Conceptos Fundamentales de Python

Funciones de Orden Superior

Métodos para Combinar Diccionarios

1. Usando el operador | (Python 3.9+)

La forma más concisa para combinar múltiples diccionarios, donde los diccionarios posteriores sobrescriben las claves duplicadas:

datos1 = {"a": 1, "b": 2}
datos2 = {"b": 3, "c": 4}
datos3 = {"d": 5}

combinado = datos1 | datos2 | datos3
print(combinado)  # Salida: {'a': 1, 'b': 3, 'c': 4, 'd': 5}

2. Usando la sintaxis de desempaquetado **

Compatible con Python 3.5+, desempaqueta los pares clave-valor en un nuevo diccionario:

combinado = {**datos1, **datos2, **datos3}
print(combinado)  # Salida: {'a': 1, 'b': 3, 'c': 4, 'd': 5}

3. Usando el método update()

Modifica el primer diccionario en su lugar, adecuado cuando no necesitas conservar el diccionario original:

combinado = datos1.copy()  # Copia para no modificar el original
combinado.update(datos2)
combinado.update(datos3)
print(combinado)  # Salida: {'a': 1, 'b': 3, 'c': 4, 'd': 5}

4. Usando collections.ChainMap

Crea una vista en cadena sin realmente combinar los diccionarios, buscando en secuencia (útil para configuración múltiple):

from collections import ChainMap

combinado = ChainMap(datos1, datos2, datos3)
print(combinado["b"])  # Salida: 2 (toma el primer valor encontrado)
print(dict(combinado))  # Conversión a diccionario normal: {'a': 1, 'b': 2, 'c': 4, 'd': 5}

5. Manejo dinámico de diccionarios

Usando bucles o functools.reduce para combinar una lista de diccionarios:

from functools import reduce

lista_diccionarios = [datos1, datos2, datos3]
combinado = reduce(lambda x, y: {**x, **y}, lista_diccionarios)
print(combinado)  # Salida: {'a': 1, 'b': 3, 'c': 4, 'd': 5}

Puntos clave:

  • Manejo de conflictos: Todos los métodos (excepto ChainMap) sobrescriben claves duplicadas con valores de diccionarios posteriores.
  • Eficiencia de memoria: ChainMap es el más eficiente ya que no copia datos, solo crea una vista.
  • Compatibilidad: El operador | fue introducido en Python 3.9; versiones anteriores requieren ** o update().

Módulo io.StringIO

Proporciona una forma de simular operaciones de archivo en memoria, permitiendo manipular cadenas como si fueran archivos.

1. Pruebas unitarias

Al escribir pruebas unitarias, a veces se necesita simular operaciones de entrada/salida de archivos. StringIO permite evitar la creación de archivos reales, haciendo las pruebas más eficientes e independientes.

import io
from unittest.mock import patch

def leer_contenido(archivo):
    return archivo.read()

def prueba_leer_contenido():
    contenido_prueba = "Este es un texto de prueba."
    archivo_mock = io.StringIO(contenido_prueba)
    resultado = leer_contenido(archivo_mock)
    assert resultado == contenido_prueba

prueba_leer_contenido()

2. Procesamiento de datos

Cuando se necesita procesar y transformar datos de texto, StringIO puede actuar como un búfer temporal, evitando lecturas y escrituras de archivos frceuentes.

import io

# Supongamos que tenemos una cadena con múltiples líneas de datos
datos = "1,2,3\n4,5,6\n7,8,9"
# Crear objeto StringIO
buffer = io.StringIO(datos)

# Procesar cada línea de datos
lineas_procesadas = []
for linea in buffer:
    numeros = [int(num) for num in linea.strip().split(',')]
    cuadrados = [num ** 2 for num in numeros]
    lineas_procesadas.append(','.join(map(str, cuadrados)))

# Combinar los datos procesados en una sola cadena
datos_procesados = '\n'.join(lineas_procesadas)
print(datos_procesados)

3. Registro de logs

En algunos casos, puedes querer almacenar información de logs en memoria en lugar de escribir directamente en un archivo. StringIO puede usarse para este propósito.

import io
import logging

# Crear objeto StringIO como destino de salida para logs
buffer_log = io.StringIO()

# Configurar el registrador
logging.basicConfig(stream=buffer_log, level=logging.INFO)
registrador = logging.getLogger()

# Registrar algunos mensajes
registrador.info("Este es un mensaje de información.")
registrador.warning("Este es un mensaje de advertencia.")

# Obtener el contenido del log
contenido_log = buffer_log.getvalue()
print(contenido_log)

Descriptores

Un descriptor en Python es un concepto útil que permite personalizar el comportamiento del acceso a atributos. Básicamente, un descriptor es una clase que implementa uno o más de los métodos __get__, __set__ y __delete__, que corresponden a la obtención, establecimiento y eliminación de atributos, respectivamente.

Métodos del protocolo de descriptor:

  1. __get__(self, instance, owner)
  • Se utiliza para obtener el valor del atributo
  • instance: La instancia que llama al descriptor (si es una llamada de instancia)
  • owner: La clase propietaria
  1. __set__(self, instance, value)
  • Se utiliza para establecer el valor del atributo
  • instance: La instancia que llama al descriptor
  • value: El valor a establecer
  1. __delete__(self, instance)
  • Se utiliza para eliminar el atributo
  • instance: La instancia que llama al descriptor

Tipos de descriptores:

  • Descriptor de datos: Implementa tanto __get__ como __set__. Tiene mayor prioridad que los atributos de instancia.
  • Descriptor no de datos: Solo implementa __get__ (como funciones y métodos). Tiene menor prioridad que los atributos de instancia.
class NumeroPositivo:
    def __set__(self, instance, valor):
        if valor <= 0:
            raise ValueError("El valor debe ser positivo")
        instance.__dict__[self.nombre] = valor  # Almacenar en el diccionario de la instancia
    
    def __get__(self, instance, owner):
        if instance is None:
            return self
        return instance.__dict__.get(self.nombre)  # Leer del diccionario de la instancia
    
    def __set_name__(self, owner, nombre):
        self.nombre = nombre  # Almacenar el nombre del atributo (compatible con Python 3.6+)

class Rectangulo:
    ancho = NumeroPositivo()
    alto = NumeroPositivo()

r1 = Rectangulo()
r2 = Rectangulo()

r1.ancho = 5  # Almacenado en el diccionario de r1
r2.ancho = 10  # Almacenado en el diccionario de r2

print(r1.ancho)  # 5 (leído del diccionario de r1)
print(r2.ancho)  # 10 (leído del diccionario de r2)

Cadenas Unicode

Definición básica

  • Unicode: Es un conjunto de caracteres que asigna un número único (llamado punto de código) a casi todos los caracteres del mundo (incluyendo letras, símbolos, emojis, etc.).
  • Ejemplo:
  • A tiene el punto de código Unicode U+0041
  • tiene el punto de código Unicode U+4E2D
  • 😊 tiene el punto de código Unicode U+1F60A
  • UTF-8: Es un esquema de codificación que convierte los puntos de código Unicode en secuencias de bytes que la computadora puede almacenar y transmitir.
  • Es una implementación de Unicode (no la única; existen también UTF-16, UTF-32, etc.)

En programación:

  • En Python 3, las cadenas (str) son Unicode por defecto.
  • Se usan encode()/decode() para convertir entre cadenas Unicode y bytes UTF-8.

Diferencias entre Python 2 y 3:

  • En Python 2, las cadenas comunes se almacenaban como ASCII de 8 bits, mientras que las cadenas Unicode se almacenaban como cadenas Unicode de 16 bits.
  • En Python 3, todas las cadenas son Unicode por defecto.

Gestores de contexto en Python

1. Definición básica

Un gestor de contexto es un objeto que implementa los métodos __enter__() y __exit__(), definiendo las acciones que deben ocurrir antes y después de la ejecución de un bloque de código.

2. Funcionamiento

Flujo de ejecución:

with GestorDeContexto() as x:
    # Bloque de código
    ...

Orden real de ejecución:

  1. Se llama a GestorDeContexto().__enter__()
  2. El valor devuelto por __enter__() se asigna a x (si hay una cláusula as)
  3. Se ejecuta el bloque de código del with
  4. Se llama a __exit__() independientemente de si el bloque terminó correctamente o con una excepción

Manejo de excepciones personalizadas:

class ManejadorDeErrores:
    def __enter__(self):
        return self
    
    def __exit__(self, exc_type, exc_val, exc_tb):
        if exc_type is ValueError:
            print("Manejando ValueError")
            return True  # Suprime la excepción
        # Otras excepciones se propagan normalmente

with ManejadorDeErrores():
    raise ValueError("Error de prueba")  # Esta excepción será capturada

El módulo estándar de Python incluye muchos gestores de contexto útiles:

  • open(): Para operaciones con archivos
  • threading.Lock(): Para bloqueos de hilos
  • decimal.localcontext(): Para contexto de operaciones Decimal
  • tempfile.TemporaryFile(): Para archivos temporales
  • unittest.mock.patch(): Para mocking en pruebas

Métodos __enter__ y __exit__ en Python

Estos dos métodos especiales conforman el protocolo de gestor de contexto de Python, que implementa la funcionalidad de la sentencia with. Su propósito principal es la gestión de recursos y el manejo de excepciones.

Funciones principales:

1. Método __enter__

  • Se llama al entrar en el contexto: Se ejecuta al inicio de la sentencia with.
  • Valor de retorno: Puede devolver un objeto que será recibido por la palabra clave as.
  • Uso típico: Asignación de recursos (como abrir archivos, obtener bloqueos, conectar bases de datos).

2. Método __exit__

  • Se llama al salir del contexto: Se ejecuta después de que el bloque de código del with termine.
  • Manejo de excepciones: Puede capturar y manejar excepciones que ocurran en el bloque de código.
  • Uso típico: Liberación de recursos (como cerrar archivos, liberar bloqueos, desconectar bases de datos).

Parámetros de __exit__

Cuando ocurre una excepción en el bloque de código del with, __exit__ recibe información sobre la excepción:

  1. exc_type: Tipo de excepción (como ValueError)
  2. exc_val: Instancia del objeto de excepción
  3. exc_tb: Objeto traceback
class ManejadorDeArchivos:
    def __init__(self, nombre_archivo, modo):
        self.nombre_archivo = nombre_archivo
        self.modo = modo
    
    def __enter__(self):
        self.archivo = open(self.nombre_archivo, self.modo)
        return self.archivo  # Este valor será recibido por as
    
    def __exit__(self, exc_type, exc_val, exc_tb):
        self.archivo.close()
        # Si devuelve True, no se lanzará la excepción del bloque with
        # Si devuelve None o False, la excepción se lanzará normalmente

# Ejemplo de uso
with ManejadorDeArchivos('prueba.txt', 'w') as f:
    f.write('¡Hola, gestor de contexto!')
# El archivo se cerrará automáticamente, incluso si ocurre una excepción durante la escritura

Resumen de características clave:

  1. Seguridad de recursos: Asegura que los recursos siempre se liberen correctamente.
  2. Manejo de excepciones: Permite manejar excepciones de manera elegante en los bloques de código.
  3. Sintaxis concisa: Evita la necesidad de múltiples sentencias try-finally.
  4. Composibilidad: Varios gestores de contexto pueden anidarse.
with open('a.txt') as f1, open('b.txt') as f2:
    # Procesar ambos archivos simultáneamente
    datos1 = f1.read()
    datos2 = f2.read()
# Ambos archivos se cerrarán automáticamente

Uso de yield

yield es una palabra clave en Python utilizada para crear generadores. Permite que una función se convierta en una función generadora, implementando cómputo perezoso y mantenimiento de estado.

1. Creación de un generador simple

def generador_simple():
    yield 1
    yield 2
    yield 3

gen = generador_simple()
print(next(gen))  # Salida: 1
print(next(gen))  # Salida: 2
print(next(gen))  # Salida: 3
# print(next(gen))  # Lanza excepción StopIteration

2. Expresiones generadoras

numeros = (x for x in range(10) if x % 2 == 0)
print(list(numeros))  # Salida: [0, 2, 4, 6, 8]

3. Uso del método send() para pasar valores

def acumulador():
    total = 0
    while True:
        valor = yield total
        if valor is None:
            break
        total += valor

acc = acumulador()
next(acc)  # Iniciar el generador
print(acc.send(10))  # Salida: 10
print(acc.send(20))  # Salida: 30
print(acc.send(5))   # Salida: 35
acc.close()  # Cerrar el generador

4. Sintaxis yield from (Python 3.3+)

Delega la producción de valores a otro generador:

def generador_secundario():
    yield 1
    yield 2

def generador_principal():
    yield 'inicio'
    yield from generador_secundario()
    yield 'fin'

for item in generador_principal():
    print(item)
# Salida: inicio 1 2 fin

5. Implementación de corrutinas

yield puede usarse para implementar corrutinas simples:

def corrutina():
    while True:
        recibido = yield
        print(f"Recibido: {recibido}")

co = corrutina()
next(co)  # Iniciar la corrutina
co.send("Mensaje 1")  # Salida: Recibido: Mensaje 1
co.send("Mensaje 2")  # Salida: Recibido: Mensaje 2

Función sorted

sorted() es una función de ordenamiento de orden superior incorporada en Python, que ordena objetos iterables y devuelve una nueva lista ordenada.

sorted(iterable, *, key=None, reverse=False)

Parámetro Descripción
iterable Objeto iterable a ordenar (listas, tuplas, cadenas, diccionarios, conjuntos, etc.)
key Función para extraer una clave de comparación de cada elemento (opcional)
reverse Booleano, True para orden descendiente, False para ascendiente (predeterminado False)

Ejemplos:

# Ordenación de tuplas
tuplas = [(1, 'b'), (2, 'a'), (0, 'a')]
tuplas_ordenadas = sorted(tuplas, key=lambda x: x[0])
print(tuplas_ordenadas)

# Ordenación por longitud de cadena
palabras = ["banana", "tarta", "manzana", "naranja"]
palabras_ordenadas = sorted(palabras, key=lambda x: len(x))
print(palabras_ordenadas)  # Salida: ['tarta', 'manzana', 'banana', 'naranja']

# Ordenación multi-criterio
estudiantes = [
    {"nombre": "Ana", "puntuacion": 85},
    {"nombre": "Luis", "puntuacion": 90},
    {"nombre": "Carlos", "puntuacion": 85}
]

estudiantes_ordenados = sorted(estudiantes, key=lambda x: (-x["puntuacion"], x["nombre"]))
print(estudiantes_ordenados)

# Ordenación de diccionario por clave o valor
notas = {'Ana': 85, 'Luis': 90, 'Carlos': 78}

# Ordenado por clave
ordenado_por_nombre = sorted(notas.items())
print(ordenado_por_nombre)  # [('Ana', 85), ('Carlos', 78), ('Luis', 90)]

# Ordenado por valor
ordenado_por_nota = sorted(notas.items(), key=lambda x: x[1])
print(ordenado_por_nota)  # [('Carlos', 78), ('Ana', 85), ('Luis', 90)]

Módulo collections

1. defaultdict

Función: Diccionario con valores por defecto, que no lanza KeyError al acceder a claves inexistentes.

Casos de uso: Conteo, agrupación, construcción de diccionarios de diccionarios, etc.

from collections import defaultdict

# Ejemplo 1: Conteo de palabras apariciones
conteo_palabras = defaultdict(int)  # Valor por defecto: 0
for palabra in ['manzana', 'banana', 'manzana', 'naranja']:
    conteo_palabras[palabra] += 1
# defaultdict(<class 'int'>, {'manzana': 2, 'banana': 1, 'naranja': 1})

# Ejemplo 2: Agrupación por inicial
palabras_por_inicial = defaultdict(list)  # Valor por defecto: lista vacía
for palabra in ['manzana', 'banana', 'naranja', 'aguacate']:
    palabras_por_inicial[palabra[0]].append(palabra)
# defaultdict(<class 'list'>, {'m': ['manzana'], 'b': ['banana'], 'n': ['naranja'], 'a': ['aguacate']})

# Agrupación por categoría
datos = [('manzana', 'fruta'), ('banana', 'fruta'), ('zanahoria', 'verdura')]
categorias = defaultdict(list)

for nombre, categoria in datos:
    categorias[categoria].append(nombre)

print(dict(categorias))

Comparación con dict.setdefault():

d = {'a': 1, 'b': 2}

# Clave inexistente
print(d.setdefault('c', 3))  # Salida: 3
print(d)  # Salida: {'a': 1, 'b': 2, 'c': 3}

2. Counter

Función: Contador para contar apariciones de objetos hashables.

Casos de uso: Estadística de frecuencia, problemas TopN, etc.

from collections import Counter

# Ejemplo 1: Conteo de elementos
cnt = Counter(['rojo', 'azul', 'rojo', 'verde', 'azul', 'azul'])
print(cnt)  # Counter({'azul': 3, 'rojo': 2, 'verde': 1})

# Ejemplo 2: Obtener los 2 elementos más comunes
print(cnt.most_common(2))  # [('azul', 3), ('rojo', 2)]

# Ejemplo 3: Conteo de frecuencia de palabras en texto
palabras = "cuántas veces aparece cada palabra en esta oración".split()
conteo_palabras = Counter(palabras)
print(conteo_palabras.most_common(3))

3. deque

Función: Cola de doble extremo, segura para hilos, permite operaciones eficientes en ambos extremos.

Casos de uso: Colas, pilas, ventanas deslizantes, búsqueda en anchura, etc.

from collections import deque

# Ejemplo 1: Uso como cola
d = deque()
d.append('a')  # Agregar al extremo derecho
d.appendleft('b')  # Agregar al extremo izquierdo
print(d)  # deque(['b', 'a'])
d.pop()  # Eliminar del extremo derecho
d.popleft()  # Eliminar del extremo izquierdo

# Ejemplo 2: Cola de tamaño fijo
ultimas_tres = deque(maxlen=3)
for i in range(5):
    ultimas_tres.append(i)
    print(ultimas_tres)
# deque([0], maxlen=3)
# deque([0, 1], maxlen=3)
# deque([0, 1, 2], maxlen=3)
# deque([1, 2, 3], maxlen=3)

4. namedtuple

Función: Tupla con nombre, crea una subclase de tupla con campos con nombre.

Casos de uso: Reemplazo de clases simples para código más legible.

from collections import namedtuple

# Ejemplo 1: Definir estructura Punto
Punto = namedtuple('Punto', ['x', 'y'])
p = Punto(11, y=22)  # Se puede crear con posición o argumentos por palabra clave
print(p.x, p.y)  # 11 22
print(p[0], p[1])  # 11 22 (también soporta acceso por índice)

# Ejemplo 2: Reemplazo de clase simple
Persona = namedtuple('Persona', ['nombre', 'edad', 'genero']
juan = Persona(nombre='Juan', edad=30, genero='masculino')
print(f"{juan.nombre} tiene {juan.edad} años")

5. OrderedDict (en Python 3.7+ dict ya está ordenado)

Función: Diccionario ordenado, recuerda el orden de inserción de claves.

Casos de uso: Operaciones con diccionario que requieren mantener el orden de inserción.

from collections import OrderedDict

# Ejemplo 1: Mantener orden de inserción
d = OrderedDict()
d['a'] = 1
d['b'] = 2
d['c'] = 3
print(list(d.keys()))  # ['a', 'b', 'c']

6. ChainMap

Función: Enlaza múltiples diccionarios o mapeos en una sola vista.

Casos de uso: Configuración multinivel, simulación de ámbitos de variables, etc.

from collections import ChainMap

# Ejemplo 1: Combinar múltiples diccionarios
dict1 = {'a': 1, 'b': 2}
dict2 = {'b': 3, 'c': 4}
combinado = ChainMap(dict1, dict2)
print(combinado['a'])  # 1 (de dict1)
print(combinado['b'])  # 2 (de dict1, tiene prioridad)
print(combinado['c'])  # 4 (de dict2)

JSON

JSON (Notación de Objetos JavaScript) es un formato ligero de intercambio de datos de texto.

import json

# Diccionario Python convertido a objeto JSON
datos1 = {
    'no': 1,
    'nombre': 'Ejemplo',
    'url': 'http://www.ejemplo.com'
}

json_str = json.dumps(datos1)
print ("Datos Python originales:", repr(datos1))
print ("Objeto JSON:", json_str)

# Objeto JSON convertido a diccionario Python
datos2 = json.loads(json_str)
print ("datos2['nombre']: ", datos2['nombre'])
print ("datos2['url']: ", datos2['url'])

Para manejar archivos en lugar de cadenas, se pueden usar json.dump() y json.load() para codificar y decodificar datos JSON:

import json

# Preparar estructura de datos Python para almacenar
datos = {
    "nombre": "María",
    "edad": 25,
    "es_estudiante": False,
    "cursos": ["Matemáticas", "Física", "Programación"],
    "direccion": {
        "calle": "Calle Principal 123",
        "ciudad": "Madrid"
    }
}

# Escribir datos en archivo JSON
with open('usuario_datos.json', 'w', encoding='utf-8') as f:
    json.dump(datos, f, ensure_ascii=False, indent=4)

print("Datos escritos en usuario_datos.json")

# Leer datos desde archivo JSON
with open('usuario_datos.json', 'r', encoding='utf-8') as f:
    datos_cargados = json.load(f)

print("Datos cargados desde archivo:")
print(datos_cargados)

Diferencias entre JSON y diccionario Python:

Característica JSON Diccionario Python
Naturaleza Formato de intercambio de datos (cadena) Estructura de datos incorporada (objeto en memoria)
Tipo de clave Solo cadenas (con comillas dobles ") Cualquier tipo hashable (cadenas, números, tuplas, etc.)
Tipo de valor Soporta cadenas, números, booleanos, null, listas, objetos JSON Soporta todos los tipos de datos de Python (como None, tuple, set, funciones, etc.)
Detalles de sintaxis Cadenas deben usar comillas dobles "; no puede haber coma final Cadenas pueden usar comillas simples ' o dobles "; puede haber coma final
Booleanos true, false (minúsculas) True, False (mayúsculas)
Nulo null None
Comentarios No soporta comentarios Soporta comentarios (como parte del código Python)
Uso Transferencia de datos (APIs), almacenamiento de datos Procesamiento interno de datos en Python

Expresiones Regulares

Patrones de expresiones regulares pueden incluir:

  • Caracteres literales: letras, números, espacios, etc., que coinciden consigo mismos.
  • Caracteres especiales: como . , *, +, ?, etc., que tienen significado especial.
Carácter Descripción Ejemplo Nota mnemotécnica
. Coincide con cualquier carácter (excepto salto de línea) a.c'abc', 'a c' Cualquier punto
[ ] Coincide con cualquier carácter dentro de los corchetes [abc] coincide con 'a', 'b' o 'c' Como una lista de compra, elige uno
` ` O lógico a|b'a' o 'b'
( ... ) Agrupación y captura (ab)+'abab' Paréntesis → "agrupar como un conjunto"
\d Dígito ([0-9]) \d+'123' Dígito
\D No dígito ([^0-9]) \D+'abc'
\s Carácter de espacio (espacio, tabulación, etc.) a\sb'a b' Espacio
\S No carácter de espacio \S+'abc'
\w Carácter de palabra ([a-zA-Z0-9_]) \w+'abc_123' Palabra
\W No carácter de palabra \W+'@#$'
* 0 o más veces a*'', 'a', 'aa' Como un cero
+ 1 o más veces a+'a', 'aa' Como un signo más, al menos uno
? 0 o 1 vez a?'', 'a' ¿Cero o uno?
{n} Exactamente n veces a{2}'aa'
{n,} Al menos n veces a{2,}'aa', 'aaa'
{n,m} Entre n y m veces a{2,3}'aa', 'aaa'
^ Coincide con el inicio de la cadena ^abc → comienza con 'abc' Flecha hacia arriba, apunta al inicio
$ Coincide con el final de la cadena abc$ → termina con 'abc' Signo de dólar al final
\b Límite de palabra \bword → palabras que comienzan con "word" b de boundary (límite)
\B No límite de palabra

Ejemplo: Validación de correo electrónico

  • Patrón básico: \w+@\w+\.\w+
  • \w+ es carácter de palabra, @ y . son literales → usuario@ejemplo.com
  • Donde \\. escapa el punto para que sea tratado literalmente

Para una validación más realista se necesita una expresión regular más estricta:

r'^[\w.-]+@[\w.-]+\.[a-zA-Z]{2,}$'

En expresiones regulares, los siguientes caracteres deben escaparse con \ para coincidir literalmente:

.  *  ?  +  ^  $  [  ]  {  }  (  )  |  \

Uso de raise

1. Uso básico

1.1 Lanzar excepciones incorporadas

# Lanzar un ValueError simple
raise ValueError("Este es un mensaje de error")

# Equivalente a
raise ValueError("Este es un mensaje de error")

Salida:

Traceback (most recent call last):
  File "ejemplo.py", line 1, in <module>
    raise ValueError("error")
ValueError: error

1.2 Relanzar la excepción actual

En un bloque de manejo de excepciones, se puede usar raise sin argumentos para relanzar la excepción actual que se está manejando:

try:
    # Código que podría generar un error
    x = 1 / 0
except ZeroDivisionError:
    print("Capturado error de división por cero")
    raise  # Relanza la misma excepción

Relación con try-except: raise y try-except suelen usarse juntos:

def procesar_datos(datos):
    if not datos:
        raise ValueError("Los datos no pueden estar vacíos")
    # Procesar datos...

try:
    procesar_datos(None)
except ValueError as e:
    print(f"Capturado error: {e}")

Módulo pickle (serialización y deserialización)

pickle es un módulo estándar de Python para la serialización y deserialización de objetos (persistencia de objetos de Python). Permite convertir objetos de Python en un flujo de bytes (serialización) y reconstruir objetos a partir de ese flujo (deserialización).

# Serialización

import pickle

datos = {
    'nombre': 'Ana',
    'edad': 30,
    'aficiones': ['lectura', 'senderismo']
}

# Serializar a archivo
with open('datos.pkl', 'wb') as f:
    pickle.dump(datos, f)

# Serializar a bytes
datos_bytes = pickle.dumps(datos)
print(datos_bytes)  # Salida de bytes


# Deserialización

# Deserializar desde archivo
with open('datos.pkl', 'rb') as f:
    datos_cargados = pickle.load(f)
    print(datos_cargados)

# Deserializar desde bytes
datos_originales = pickle.loads(datos_bytes)
print(datos_originales)

Destrucción de objetos (recolección de basura) en Python

En Python, la recolección de objetos se maneja a través del mecanismo de recolección de basura (Garbage Collection, GC). Python utiliza el conteo de referencias como mecanismo principal de gestión de memoria, combinado con un recolector de basura circular para manejar referencias circulares. A continuación se detalla la recolección de objetos en Python.

1. Conteo de referencias (Reference Counting)

Cada objeto en Python tiene un conteo de referencias que registra cuántas referencias apuntan a ese objeto. Cuando el conteo de referencias llega a 0, el objeto se recupera inmediatamente.

Reglas del conteo de referencias:

  • Cuando se crea un objeto, su conteo de referencias es 1.
  • Cuando un objeto se asigna a una variable, se agrega a un contenedor (como listas, diccionarios) o se pasa como parámetro, el conteo de referencias aumenta.
  • Cuando una variable se reasigna, sale del ámbito o se elimina un contenedor, el conteo de referencias disminuye.
  • Cuando el conteo de referencias llega a 0, el objeto se destruye inmediatamente y se libera la memoria.
import sys

# Crear un objeto
a = [1, 2, 3]
print(sys.getrefcount(a))  # Salida de conteo de referencias, aquí es 2 (a + parámetro getrefcount)

# Aumentar referencia
b = a
print(sys.getrefcount(a))  # Salida de conteo de referencias, aquí es 3 (a, b + parámetro getrefcount)

# Disminuir referencia
del b
print(sys.getrefcount(a))  # Salida de conteo de referencias, aquí es 2 (a + parámetro getrefcount)

# Disminuir referencia
del a
# Ahora el conteo de referencias es 0, el objeto se recupera

2. Recolector de basura circular (Cycle Detector)

El conteo de referencias no puede manejar referencias circulares. Por ejemplo, si dos objetos se refieren mutuamente pero no hay referencias externas que apunten a ellos, su conteo de referencias nunca llegará a 0, causando una fuga de memoria. Para resolver esto, Python introduce el recolector de basura circular.

Principio de funcionamiento del recolector de basura circular:

  • El recolector de basura de Python verifica periódicamente las relaciones de referencias entre objetos.
  • Si detecta un grupo de objetos que se refieren mutuamente pero no hay referencias externas que apunten a ellos, marca este grupo de objetos como basura y los recupera.
class Nodo:
    def __init__(self, valor):
        self.valor = valor
        self.siguiente = None

# Crear referencia circular
nodo1 = Nodo(1)
nodo2 = Nodo(2)
nodo1.siguiente = nodo2
nodo2.siguiente = nodo1

# Eliminar referencias externas
del nodo1
del nodo2

# Ahora nodo1 y nodo2 se refieren mutuamente, pero su conteo de referencias no es 0
# El recolector de basura circular detectará esta situación y los recuperará

3. Disparo de la recolección de basura

El mecanismo de recolección de basura de Python se dispara en los siguientes casos:

  1. Conteo de referencias llega a 0: El objeto se recupera inmediatamente.
  2. Llamada explícita a gc.collect(): Disparo manual de la recolección de basura.
  3. Alcanzar umbral: El recolector de basura de Python ajusta dinámicamente los umbrales según la asignación y liberación de objetos. Cuando se alcanza el umbral, se dispara automáticamente la recolección de basura.

Ejemplo: Disparo manual de recolección de basura

import gc

# Crear algunos objetos
a = [1, 2, 3]
b = [4, 5, 6]
a.append(b)
b.append(a)

# Eliminar referencias externas
del a
del b

# Disparar manualmente la recolección de basura
gc.collect()  # Devuelve el número de objetos recuperados

4. Método __del__

Python proporciona un método __del__ que puede realizar operaciones de limpieza cuando un objeto se recupera. Sin embargo, se debe tener en cuenta:

  • El momento de llamada a __del__ depende del recolector de basura, no se garantiza que se ejecute inmediatamente.
  • Si hay referencias circulares entre objetos, __del__ podría no ser llamado.
class MiClase:
    def __init__(self, nombre):
        self.nombre = nombre
    
    def __del__(self):
        print(f"El objeto {self.nombre} está siendo destruido")

# Crear objeto
obj = MiClase("ejemplo")

# Eliminar objeto
del obj  # Salida: El objeto ejemplo está siendo destruido

Método seek en archivos

En Python, el error io.UnsupportedOperation: can't do nonzero cur-relative seeks ocurre generalmente cuando se intenta usar el método seek() en un archivo abierto en modo texto ('r' o 'w'), pasando un offset distinto de cero y from=1 (relativo a la posición actual) o from=2 (relativo al final del archivo).

Causa del error

En modo texto, el objeto de archivo de Python no soporta desplazamientos no cero relativos a la posición actual o al final del archivo (cur-relative seeks o end-relative seeks). Esto se debe a que el contenido del archivo en modo texto puede estar codificado (como UTF-8), lo que hace que los desplazamientos de bytes no coincidan con los desplazamientos de caracteres, imposibilitando el cálculo preciso de posiciones.

Métodos de solución

Método 1: Usar modo binario

Si necesitas usar seek() con desplazamientos relativos a la posición actual o al final del archivo, puedes abrir el archivo en modo binario ('rb' o 'wb'). En modo binario, el comportamiento de seek() es preciso y soporta cualquier desplazamiento y posición de referencia.

with open('ejemplo.txt', 'rb') as f:  # Abrir archivo en modo binario
    f.seek(5, 1)  # Mover 5 bytes desde la posición actual
    datos = f.read(10)  # Leer 10 bytes
    print(datos)

Pero en este caso se devuelve texto en formato binario, que necesita ser decodificado:

datos = datos.decode(encoding="utf-8", errors="strict") 

Método 2: Usar solo seek() desde el inicio del archivo

Si debes operar en modo texto, puedes usar solo seek() desde el inicio del archivo (from=0), que es el único soportado en modo texto.

with open('ejemplo.txt', 'r') as f:  # Abrir archivo en modo texto
    f.seek(10)  # Mover 10 bytes desde el inicio del archivo
    datos = f.read(5)  # Leer 5 caracteres
    print(datos)

Método 3: Calcular posición manualmente

Si necesitas mover el puntero desde el final del archivo o desde la posición actual, puedes usar tell() para obtener la posición actual, calcular manualmente el desplazamiento y luego usar seek(offset, 0).

with open('ejemplo.txt', 'r') as f:
    pos_actual = f.tell()  # Obtener posición actual
    f.seek(pos_actual + 5, 0)  # Calcular manualmente el desplazamiento, mover desde el inicio
    datos = f.read(5)
    print(datos)

Comprensiones de lista

Ejemplo:

# Generar lista de cuadrados
cuadrados = [x**2 for x in range(10])   


# Comprensión de lista anidada
matriz = [[i + j for j in range(3)] for i in range(3)]

Equivalente a:

matriz = []  # Inicializar lista vacía
for i in range(3):  # Bucle externo, i de 0 a 2
    fila = []  # Inicializar fila vacía
    for j in range(3):  # Bucle interno, j de 0 a 2
        fila.append(i + j)  # Agregar resultado i+j a la fila actual
    matriz.append(fila)  # Agregar fila actual a la matriz

 
# Generación de lista con condiciones 

numeros_positivos_pequeños = [i for i in numeros if i > 0 and i < 5]

Comparación entre comprensión de lista y map/filter:

# Usando map y filter
numeros = [1, 2, 3, 4, 5]
cuadrados_pares = list(map(lambda x: x ** 2, filter(lambda x: x % 2 == 0, numeros)))

# Usando comprensión de lista
numeros = [1, 2, 3, 4, 5]
cuadrados_pares = [x ** 2 for x in numeros if x % 2 == 0]

Paso de parámetros en funciones de Python

  • Tipos inmutables: Similar a paso por valor en C++, como enteros, cadenas, tuplas. Al llamar a una función con un parámetro como fun(a), se pasa solo el valor de a, sin afectar el objeto a en sí. Por ejemplo, modificar el valor de a dentro de fun(a) solo modifica otro objeto copiado, no afecta al a original.
  • Tipos mutables: Similar a paso por referencia en C++, como listas, diccionarios. Al llamar a fun(lista), se pasa realmente el objeto lista, y las modificaciones dentro de la función afectarán al lista externo.

Módulo calendar

(1) Verificación de año bisiesto

Usar la función isleap() para determinar si un año es bisiesto.

import calendar

print(calendar.isleap(2024))  # True
print(calendar.isleap(2023))  # False

Etiquetas: Python diccionarios generadores Serialización expresiones-regulares

Publicado el 6-5 03:27