Archivos y Persistencia de Datos
Domina el arte de leer, escribir y persistir datos en Python
Las aplicaciones del mundo real no solo ejecutan código en memoria y terminan sin dejar rastro. Interactúan con redes, discos y bases de datos. Intercambian información con otras aplicaciones usando formatos adecuados para cada situación.
En este capítulo exploraremos el manejo de archivos y directorios, compresión, formatos de intercambio de datos como JSON, y la persistencia de datos usando pickle, shelve y SQLAlchemy.
Trabajando con Archivos
Python ofrece herramientas intuitivas para trabajar con archivos. La función open() es tu puerta de entrada al sistema de archivos, permitiendo leer y escribir datos de manera simple y segura.
Abriendo Archivos
# Forma básica (NO recomendada) fh = open("data.txt", "rt") # r: read, t: text for line in fh.readlines(): print(line.strip()) fh.close() # ¡No olvides cerrar! # Forma segura con try/finally fh = open("data.txt") try: for line in fh: print(line.strip()) finally: fh.close() # Siempre se ejecuta
Context Managers: La Forma Pythonica
Los context managers garantizan que el archivo se cierre automáticamente, incluso si ocurre un error:
# ✓ RECOMENDADO: Usando context manager with open("fear.txt") as fh: for line in fh: print(line.strip()) # El archivo se cierra automáticamente aquí # Lectura y escritura with open("source.txt") as f: lines = [line.rstrip() for line in f] with open("copy.txt", "w") as fw: fw.write("\n".join(lines))
with llama automáticamente a fh.close() cuando sale del bloque, incluso si ocurre una excepción. Es la forma más segura y limpia de trabajar con archivos.
Modo Binario
Para archivos que no son texto puro (imágenes, audio, formatos propietarios), usa el modo binario:
# Escribir en modo binario with open("example.bin", "wb") as fw: fw.write(b"Binary data here...") # Leer en modo binario with open("example.bin", "rb") as f: print(f.read()) # b'Binary data here...'
Protección contra Sobreescritura
# Modo "x": solo crea si NO existe with open("new_file.txt", "x") as fw: fw.write("Primera línea") # ✓ Funciona with open("new_file.txt", "x") as fw: fw.write("Segunda línea") # ✗ FileExistsError
Pathlib: Rutas Modernas
El módulo pathlib proporciona clases que representan rutas del sistema de archivos con semánticas apropiadas para diferentes sistemas operativos.
from pathlib import Path p = Path("data.txt") print(p.absolute()) # /home/user/project/data.txt print(p.name) # data.txt print(p.parent) # . (directorio actual) print(p.suffix) # .txt print(p.stem) # data print(p.parts) # ('data.txt',) # Concatenación elegante con / readme = p.parent / ".." / "docs" / "README.md" print(readme.resolve()) # Ruta absoluta resuelta
Verificar Existencia
from pathlib import Path p = Path("config.json") print(p.exists()) # True/False print(p.is_file()) # True si es archivo print(p.is_dir()) # True si es directorio # Verificar directorio folder = Path("/Users/dev/projects") print(folder.is_dir()) # True
Manipulación de Archivos y Directorios
import shutil from pathlib import Path base = Path("project") # Crear estructura de directorios (base / "src" / "utils").mkdir(parents=True) (base / "tests").mkdir() # Crear archivos for name in ("main.py", "config.py"): with open(base / "src" / name, "w") as f: f.write(f"# {name}\n") # Renombrar old = base / "src" / "config.py" old.rename(old.parent / "settings.py") # Mover directorio completo shutil.move(base / "src", base / "source")
Archivos y Directorios Temporales
from tempfile import NamedTemporaryFile, TemporaryDirectory with TemporaryDirectory(dir=".") as td: print("Temp dir:", td) with NamedTemporaryFile(dir=td) as t: print("Temp file:", t.name) t.write(b"Temporary data") # Ambos se eliminan automáticamente al salir
Listar Contenido de Directorios
from pathlib import Path p = Path(".") # Listar todo en el directorio actual for entry in p.glob("*"): tipo = "File:" if entry.is_file() else "Dir:" print(tipo, entry) # Buscar recursivamente archivos Python for py_file in p.glob("**/*.py"): print(py_file) # Usando walk() para escanear árbol completo for root, dirs, files in p.walk(): print(f"Root: {root}") for f in files: print(f" - {f}")
Compresión de Archivos
Python permite crear y extraer archivos comprimidos en varios formatos. El formato ZIP es uno de los más comunes:
from zipfile import ZipFile # Crear archivo ZIP with ZipFile("backup.zip", "w") as zp: zp.write("config.json") zp.write("data/users.csv") zp.write("data/products.csv") # Extraer archivos específicos with ZipFile("backup.zip") as zp: zp.extract("config.json", "restored/") zp.extractall("full_restore/") # Todo
tar.gz, gzip y bz2 mediante los módulos tarfile, gzip y bz2 respectivamente.
Formato JSON
JSON (JavaScript Object Notation) es quizás el formato de intercambio de datos más usado en Python. Forma parte de la biblioteca estándar y ofrece simplicidad y compatibilidad universal.
Serialización Básica
import json data = { "name": "Sherlock Holmes", "address": { "street": "221B Baker St", "city": "London", "country": "UK" }, "cases_solved": 156 } # Serializar a string JSON json_str = json.dumps(data, indent=2, sort_keys=True) print(json_str) # Deserializar de vuelta a Python data_back = json.loads(json_str) assert data == data_back # ✓ Idénticos
"address": {
"city": "London",
"country": "UK",
"street": "221B Baker St"
},
"cases_solved": 156,
"name": "Sherlock Holmes"
}
Trabajando con Archivos
import json # Guardar en archivo with open("config.json", "w") as f: json.dump(data, f, indent=2) # Cargar desde archivo with open("config.json") as f: loaded = json.load(f)
Encoder Personalizado
JSON no puede serializar todos los tipos de Python. Para tipos personalizados, creamos un encoder:
import json class ComplexEncoder(json.JSONEncoder): def default(self, obj): if isinstance(obj, complex): return { "_meta": "complex", "num": [obj.real, obj.imag] } return super().default(obj) data = { "value": 3 + 4j, "name": "complex number" } # Serializar con encoder personalizado json_data = json.dumps(data, cls=ComplexEncoder) print(json_data) # Deserializar con object_hook def decode_complex(obj): if obj.get("_meta") == "complex": return complex(*obj["num"]) return obj restored = json.loads(json_data, object_hook=decode_complex) print(restored["value"]) # (3+4j)
I/O y Streams
Python ofrece streams en memoria que pueden ser útiles para operaciones de I/O sin tocar el disco.
StringIO: Streams en Memoria
import io with io.StringIO() as stream: stream.write("Learning Python.\n") print("Become a ninja!", file=stream) contents = stream.getvalue() print(contents) # Stream se cierra automáticamente
Become a ninja!
Peticiones HTTP con requests
import requests urls = { "ip": "https://httpbin.org/ip", "headers": "https://httpbin.org/headers", "json": "https://httpbin.org/json", } for name, url in urls.items(): resp = requests.get(url) print(f"=== {name} ===") print(resp.json()) # Auto-decode JSON
Persistencia con Pickle
El módulo pickle convierte objetos Python a flujos de bytes y viceversa. A diferencia de JSON, soporta casi cualquier tipo de dato Python, pero es específico del lenguaje.
import pickle from dataclasses import dataclass @dataclass class Person: first_name: str last_name: str id: int def greet(self): print(f"Hi, I'm {self.first_name} {self.last_name}") people = [ Person("Obi-Wan", "Kenobi", 123), Person("Anakin", "Skywalker", 456), ] # Guardar (serializar) with open("data.pickle", "wb") as f: pickle.dump(people, f) # Cargar (deserializar) with open("data.pickle", "rb") as f: loaded = pickle.load(f) for person in loaded: person.greet() # ¡Los métodos también funcionan!
Hi, I'm Anakin Skywalker
Shelve: Diccionario Persistente
Un "shelf" es un diccionario persistente. Los valores pueden ser cualquier objeto que pickle pueda serializar, dándote flexibilidad sin la complejidad de una base de datos.
import shelve class Person: def __init__(self, name, id): self.name = name self.id = id with shelve.open("myshelf.db") as db: # Guardar como diccionario db["obi1"] = Person("Obi-Wan", 123) db["ani"] = Person("Anakin", 456) db["primes"] = [2, 3, 5, 7] print(list(db.keys())) # ['obi1', 'ani', 'primes'] # Eliminar del db["ani"] # Verificar membresía print("obi1" in db) # True print("ani" in db) # False
Modo Writeback
# Sin writeback: hay que reasignar with shelve.open("shelf1.db") as db: db["nums"] = [1, 2, 3] nums = db["nums"] nums.append(4) db["nums"] = nums # Necesario # Con writeback: modificación in-place with shelve.open("shelf2.db", writeback=True) as db: db["nums"] = [1, 2, 3] db["nums"].append(4) # ¡Funciona directamente!
writeback=True consume más memoria y hace el cierre más lento, pero permite modificaciones in-place más intuitivas.
SQLAlchemy: ORM Profesional
SQLAlchemy es el ORM (Object-Relational Mapping) más popular de Python. Permite interactuar con bases de datos relacionales usando objetos Python en lugar de SQL crudo.
Definición de Modelos
from sqlalchemy import ForeignKey, String, Integer from sqlalchemy.orm import ( DeclarativeBase, mapped_column, relationship ) class Base(DeclarativeBase): pass class Person(Base): __tablename__ = "person" id = mapped_column(Integer, primary_key=True) name = mapped_column(String) age = mapped_column(Integer) emails = relationship( "Email", back_populates="person", cascade="all, delete-orphan" ) def __repr__(self): return f"{self.name}(id={self.id})" class Email(Base): __tablename__ = "email" id = mapped_column(Integer, primary_key=True) email = mapped_column(String) person_id = mapped_column(ForeignKey("person.id")) person = relationship("Person", back_populates="emails")
Operaciones CRUD
from sqlalchemy import create_engine, select from sqlalchemy.orm import Session from alchemy_models import Person, Email, Base # Crear engine (en memoria para demo) engine = create_engine("sqlite:///:memory:") Base.metadata.create_all(engine) with Session(engine) as session: # CREATE anakin = Person(name="Anakin Skywalker", age=32) anakin.emails.append(Email(email="ani@example.com")) anakin.emails.append(Email(email="vader@empire.gov")) session.add(anakin) session.commit() # READ query = select(Person).where(Person.name.like("Anakin%")) result = session.scalar(query) print(result, result.emails) # UPDATE result.age = 45 session.commit() # DELETE session.delete(result) session.commit()
anakin, sus emails también se eliminan automáticamente gracias a cascade="all, delete-orphan". SQLAlchemy maneja las relaciones por ti.
Archivos de Configuración
Los archivos de configuración permiten separar settings del código, facilitando el mantenimiento y la gestión de diferentes entornos (desarrollo, producción, testing).
Formato INI
[owner] name = Fabrizio Romano dob = 1975-12-29T11:50:00Z [DEFAULT] host = 192.168.1.1 [database] host = 192.168.1.255 user = redis password = secret-password port = 6379
from configparser import ConfigParser config = ConfigParser() config.read("config.ini") print(config["database"]["host"]) # 192.168.1.255 print(config["database"]["port"]) # 6379 print(config["owner"]["name"]) # Fabrizio Romano
Formato TOML
TOML es más moderno y popular en proyectos Python. Soporta tipos de datos nativos como listas, números y fechas:
title = "Config Example" [owner] name = "Fabrizio Romano" dob = 1975-12-29T11:50:00Z [database] host = "192.168.1.255" ports = [6379, 6380] enabled = true [database.primary] port = 6379 connection_max = 5000
import tomllib with open("config.toml", "rb") as f: config = tomllib.load(f) print(config["title"]) # Config Example print(config["database"]["ports"]) # [6379, 6380] print(config["database"]["primary"]) # {'port': 6379, ...} print(config["owner"]["dob"]) # datetime object!
datetime.
Resumen del Capítulo
01 // Archivos
Usa context managers (with) para garantizar cierre automático. Pathlib para rutas modernas y portables.
02 // JSON
Formato de intercambio universal. Usa encoders personalizados para tipos no nativos.
03 // Pickle & Shelve
Serialización Python-nativa. Potente pero solo para datos confiables.
04 // SQLAlchemy
ORM profesional para bases de datos. Trabaja con objetos, no SQL crudo.
05 // I/O Streams
StringIO para operaciones en memoria. Requests para HTTP.
06 // Configuración
INI para simplicidad, TOML para tipos ricos. Separa config del código.