🎓 Para PROS

Ya dominas lo básico. Ahora vamos a ver cosas que te harán entender Python de verdad y evitar bugs raros que vuelven loco a cualquiera.

🧠
Nivel: Esto es contenido avanzado. Si algo no lo pillas a la primera, no pasa nada. Vuelve después de practicar más con lo básico.

📦 Mutables vs Inmutables

En Python, algunos objetos se pueden modificar después de crearlos y otros NO. Esto es súper importante y causa muchos bugs si no lo entiendes.

Inmutables (no se pueden cambiar)

  • int - números enteros
  • float - números decimales
  • str - strings/textos
  • tuple - tuplas
  • bool - True/False

Mutables (sí se pueden cambiar)

  • list - listas
  • dict - diccionarios
  • set - conjuntos

¿Y esto qué significa en la práctica? Mira:

# Con INMUTABLES - el original NO cambia
texto = "hola"
texto_nuevo = texto.upper()
print(texto)        # → "hola" (sigue igual)
print(texto_nuevo)  # → "HOLA" (es una copia nueva)

# Con MUTABLES - ¡OJO! el original SÍ cambia
lista = [1, 2, 3]
lista.append(4)
print(lista)  # → [1, 2, 3, 4] (la lista original cambió)
⚠️
Bug clásico: Los strings NO se modifican "in place". texto.upper() no cambia texto, devuelve una COPIA. Si quieres guardar el cambio: texto = texto.upper()

🔗 El truco de las referencias

Cuando asignas una lista a otra variable, NO se copia. ¡Apuntan al mismo sitio!

# Esto NO es una copia
original = [1, 2, 3]
copia_falsa = original  # ¡Apuntan al mismo sitio!

copia_falsa.append(4)
print(original)  # → [1, 2, 3, 4] ¡¡TAMBIÉN CAMBIÓ!!

Es como si dos personas tuvieran la dirección de la misma casa. Si uno pinta la puerta, el otro también lo ve.

¿Cómo hacer una copia de verdad?

# Opción 1: slicing (la más común)
original = [1, 2, 3]
copia_real = original[:]

# Opción 2: list()
copia_real = list(original)

# Opción 3: .copy()
copia_real = original.copy()

# Ahora sí son independientes
copia_real.append(4)
print(original)   # → [1, 2, 3] (no cambió)
print(copia_real) # → [1, 2, 3, 4]

🎯 Funciones: paso por valor vs referencia

Cuando pasas algo a una función, ¿puede la función modificar el original? Depende de si es mutable o inmutable.

Con inmutables: la función NO puede cambiar el original

def intentar_cambiar(numero):
    numero = numero + 10
    print(f"Dentro: {numero}")

x = 5
intentar_cambiar(x)
print(f"Fuera: {x}")

# Resultado:
# Dentro: 15
# Fuera: 5  ← ¡No cambió!

Con mutables: ¡SÍ puede cambiar el original!

def modificar_lista(lista):
    lista.append(999)
    print(f"Dentro: {lista}")

mi_lista = [1, 2, 3]
modificar_lista(mi_lista)
print(f"Fuera: {mi_lista}")

# Resultado:
# Dentro: [1, 2, 3, 999]
# Fuera: [1, 2, 3, 999]  ← ¡SÍ cambió!
💡
Regla fácil: Si no quieres que la función modifique tu lista original, pásale una copia: modificar_lista(mi_lista[:])

⚡ Trucos de competición

1. Operador ternario (if en una línea)

# En vez de esto:
if edad >= 18:
    estado = "mayor"
else:
    estado = "menor"

# Puedes hacer esto:
estado = "mayor" if edad >= 18 else "menor"

2. Desempaquetado múltiple

# Intercambiar dos variables sin variable temporal
a, b = b, a

# Desempaquetar listas
primero, *resto = [1, 2, 3, 4, 5]
print(primero)  # → 1
print(resto)    # → [2, 3, 4, 5]

# Primero y último
primero, *medio, ultimo = [1, 2, 3, 4, 5]
print(primero, ultimo)  # → 1 5

3. Enumerate con índice

# En vez de esto:
i = 0
for item in lista:
    print(i, item)
    i += 1

# Haz esto:
for i, item in enumerate(lista):
    print(i, item)

4. Zip para recorrer dos listas a la vez

nombres = ["Ana", "Luis", "Eva"]
notas = [8, 9, 7]

for nombre, nota in zip(nombres, notas):
    print(f"{nombre}: {nota}")

# → Ana: 8
# → Luis: 9
# → Eva: 7

5. Any y All (muy útiles)

numeros = [2, 4, 6, 8]

# ¿Todos son pares?
print(all(n % 2 == 0 for n in numeros))  # → True

# ¿Alguno es mayor que 5?
print(any(n > 5 for n in numeros))  # → True

6. Counter para frecuencias (importar)

from collections import Counter

texto = "abracadabra"
frecuencias = Counter(texto)
print(frecuencias)
# → Counter({'a': 5, 'b': 2, 'r': 2, 'c': 1, 'd': 1})

# Las 2 más comunes
print(frecuencias.most_common(2))
# → [('a', 5), ('b', 2)]

7. Defaultdict para evitar KeyError

from collections import defaultdict

# Diccionario normal: da error si la clave no existe
normal = {}
# normal["nueva"] += 1  # ¡KeyError!

# Defaultdict: crea el valor por defecto automáticamente
conteo = defaultdict(int)  # int() = 0
conteo["nueva"] += 1  # Funciona: crea 0 y suma 1

# Para listas
grupos = defaultdict(list)
grupos["A"].append("Ana")  # Crea lista vacía y añade

🧪 Funciones lambda (funciones anónimas)

Son funciones pequeñas de una sola línea. Muy útiles para ordenar.

# Función normal
def doble(x):
    return x * 2

# Lo mismo con lambda
doble = lambda x: x * 2

# Uso típico: ordenar con criterio personalizado
alumnos = [("Ana", 8), ("Luis", 9), ("Eva", 7)]

# Ordenar por nota (segundo elemento)
ordenados = sorted(alumnos, key=lambda x: x[1])
print(ordenados)  # → [('Eva', 7), ('Ana', 8), ('Luis', 9)]

# Ordenar por nota descendente
ordenados = sorted(alumnos, key=lambda x: -x[1])
print(ordenados)  # → [('Luis', 9), ('Ana', 8), ('Eva', 7)]

📐 Comprensiones avanzadas

List comprehension con condición

# Solo los pares
pares = [x for x in range(10) if x % 2 == 0]
# → [0, 2, 4, 6, 8]

# Con transformación y condición
dobles_pares = [x * 2 for x in range(10) if x % 2 == 0]
# → [0, 4, 8, 12, 16]

Dict comprehension

# Crear diccionario de cuadrados
cuadrados = {x: x**2 for x in range(1, 6)}
# → {1: 1, 2: 4, 3: 9, 4: 16, 5: 25}

# Invertir un diccionario
original = {"a": 1, "b": 2}
invertido = {v: k for k, v in original.items()}
# → {1: 'a', 2: 'b'}

🐛 Errores comunes que evitar

🛑
1. Modificar lista mientras la recorres
# ¡MAL! Comportamiento impredecible
for item in lista:
    if condicion:
        lista.remove(item)

# BIEN: crear lista nueva
lista = [item for item in lista if not condicion]
🛑
2. Lista como parámetro por defecto
# ¡MAL! La lista se comparte entre llamadas
def agregar(item, lista=[]):
    lista.append(item)
    return lista

# BIEN: usar None
def agregar(item, lista=None):
    if lista is None:
        lista = []
    lista.append(item)
    return lista
🛑
3. Comparar con == cuando debería ser "is"
# Para None, True, False usar "is"
if x is None:  # ✓ Correcto
if x == None:  # ✗ Funciona pero es feo

🏁 Resumen rápido

Concepto Qué recordar
Inmutables int, float, str, tuple → no cambian, se crean copias
Mutables list, dict, set → sí cambian, cuidado con referencias
Copiar lista lista[:] o lista.copy()
Funciones Mutables se modifican, inmutables no
Lambda lambda x: x*2 = función en una línea
Counter Cuenta frecuencias automáticamente
🎯
Consejo final: No intentes memorizar todo esto. Practica, encuentra errores, y vuelve aquí cuando algo no funcione como esperabas. Así es como se aprende de verdad.