Referencia de Python
Sintaxis, ejemplos y explicaciones breves: desde las bases del lenguaje hasta patrones algorítmicos, clases y pruebas.
Fundamentos del lenguaje13
n = 42
ratio = 1.5
name = "Ada"
flag = True
nothing = NoneEl tipo se deduce del valor; no hay que declararlo. Comprueba con type(x) o isinstance(x, int).
f"{name} solved {n} tasks"
f"{ratio:.2f}"
f"{n:>5}"
f"{n=}"Interpola expresiones en una cadena. :.2f redondea a dos decimales, :>5 alinea, {n=} imprime nombre y valor.
if items:
...
if not s:
...
bool(0), bool(""), bool([]) # FalseLas colecciones vacías, 0, "" y None son falsos. Usa `if items` en vez de `if len(items) > 0`.
a, b = b, a
first, *rest = [1, 2, 3, 4]
x = y = 0Intercambia sin variable temporal; *rest recoge el resto de la secuencia.
sign = "+" if n >= 0 else "-"Condicional en una línea. Devuelve un valor, así que se puede asignar o retornar.
7 // 2 # 3
7 % 2 # 1
divmod(7, 2) # (3, 1)
-7 // 2 # -4// redondea hacia abajo, no hacia cero: -7 // 2 es -4. divmod devuelve cociente y resto a la vez.
len([10, 20, 30]) # 3`len()` es una función incorporada que utiliza el protocolo `__len__`, por lo que funciona de forma uniforme con distintos contenedores. En Python no existe el método `.len()`.
print("a", "b", 3, sep="-", end="\n")`print()` acepta varios valores, inserta `sep` entre ellos y añade `end` después del último. Por defecto utiliza un espacio y un salto de línea.
n = int(input())`input()` siempre devuelve una cadena, incluso cuando el usuario introduce un número. `int()` lanza `ValueError` si la cadena no representa un entero válido.
s = str(42)
n = int("42")
x = float("3.5")
h = int("ff", 16)`str`, `int` y `float` crean valores del tipo solicitado, mientras que el segundo argumento de `int()` indica la base numérica. Una cadena no válida provoca `ValueError`.
isinstance(True, int) # True
type(True) is int # False`isinstance()` tiene en cuenta la herencia y normalmente es la mejor opción para comprobar un tipo. `type(x) is T` solo acepta una coincidencia exacta.
def add_item(item, items=None):
if items is None:
items = []
items.append(item)
return items`None` es un marcador seguro cuando hay que crear un objeto mutable por separado en cada llamada. Debe comprobarse con `is None`, no con `== None`.
a = [1]
b = [1]
a == b # True
a is b # False
x = 1
y = 1
x is y # True`==` compara valores, mientras que `is` comprueba si dos referencias apuntan al mismo objeto. Los enteros pequeños pueden estar internados, por lo que `is` no debe usarse para comparar números.
Control de flujo10
if score >= 90:
grade = "A"
elif score >= 75:
grade = "B"
else:
grade = "C"Las condiciones se comprueban de arriba abajo y se ejecuta la primera rama que coincida. Una condición general puede ocultar otra más específica situada después.
a == b
a != b
a < b
a <= b
a is None
0 <= x < n
a < b == c
x not in blockedUna cadena de comparaciones funciona como varias comprobaciones unidas con and, pero la expresión intermedia se evalúa una sola vez. Con None se suele comparar mediante is o is not.
value = cached or compute()
result = ready and data
flag = not itemsand y or usan evaluación de cortocircuito y devuelven uno de sus operandos, no necesariamente un bool. El operador not siempre devuelve un bool.
for item in items:
process(item)
while left < right:
left += 1for recorre un iterable, mientras que while se repite mientras su condición sea verdadera. Un while debe modificar el estado relevante o puede no terminar nunca.
range(stop)
range(start, stop)
range(start, stop, step)
range(n - 1, -1, -1)range crea una secuencia perezosa de enteros y no incluye stop. Para recorrer hacia atrás se necesita un step negativo, y el paso cero no es válido.
for x in items:
if x == target:
breakbreak termina inmediatamente el bucle más cercano. En bucles anidados, el bucle exterior continúa.
for x in items:
if x < 0:
continue
process(x)continue omite el resto de la iteración actual y pasa a la siguiente. En un while, hay que actualizar antes las variables que controlan el bucle.
for x in items:
if x == target:
found = True
break
else:
found = FalseEl bloque else de un bucle se ejecuta solo si el bucle termina sin break. También se ejecuta cuando el iterable está vacío.
match token:
case 0:
kind = "zero"
case int() if token > 0:
kind = "positive"
case _:
kind = "other"match selecciona el primer patrón que coincida y _ actúa como caso general. La ejecución no continúa automáticamente por los casos siguientes.
for key, value in pairs:
process(key, value)
for i, (x, y) in enumerate(points):
process(i, x, y)El destino del bucle puede desempaquetar directamente estructuras anidadas. El número y la forma de los valores deben coincidir salvo que se use un destino con asterisco.
Funciones10
def bounds(values):
return min(values), max(values)
low, high = bounds(items)Varios valores devueltos se empaquetan realmente en una tupla. Al desempaquetarlos, el número de destinos debe coincidir con el de elementos.
def power(base, exponent=2):
return base ** exponentEl valor predeterminado se usa cuando se omite el argumento. La expresión predeterminada se evalúa una sola vez al crear la función.
def add_item(item, items=[]):
items.append(item)
return itemsUn objeto mutable predeterminado se comparte entre llamadas, por lo que los datos pueden acumularse de forma inesperada. Normalmente se usa None y se crea el objeto dentro de la función.
def collect(first, *args, **kwargs):
return first, args, kwargs*args reúne los argumentos posicionales adicionales en una tupla y **kwargs reúne los argumentos con nombre en un diccionario. Los parámetros normales se enlazan primero.
def clamp(value, *, low=0, high=100):
return max(low, min(value, high))Los parámetros situados después de * solo pueden pasarse por nombre. Esto evita errores causados por el orden poco claro de argumentos parecidos.
key = lambda item: item[1]
result = sorted(items, key=key)lambda crea una función pequeña a partir de una sola expresión y no admite instrucciones normales. Para una lógica más compleja resulta más claro usar def.
def make_adder(delta):
def add(value):
return value + delta
return addUn cierre conserva el acceso a los nombres de la función exterior después de que esta termine. Los nombres se capturan por referencia, lo que puede causar problemas de enlace tardío en bucles.
count = 0
def outer():
total = 0
def update():
global count
nonlocal total
count += 1
total += 1
return total
return updateglobal modifica un nombre del módulo y nonlocal un nombre del ámbito exterior más cercano que ya lo define. Sin estas declaraciones, una asignación crea un nombre local.
from functools import lru_cache
@lru_cache(maxsize=None)
def fib(n):
if n < 2:
return n
return fib(n - 1) + fib(n - 2)lru_cache guarda los resultados de las llamadas y resulta especialmente útil para subproblemas recursivos repetidos. Los argumentos deben ser hashables y maxsize=None crea una caché sin límite.
def binary_search(a: list[int], target: int) -> int | None:
...Las anotaciones describen los tipos esperados de los parámetros y del resultado, pero por sí solas no los comprueban en tiempo de ejecución. X | None indica que el resultado puede estar ausente.
Cadenas7
s[1:4]
s[:3]
s[-2:]
s[::-1]
s[::2]El límite derecho no se incluye. s[::-1] invierte la cadena, s[::2] toma un carácter de cada dos, los índices negativos cuentan desde el final.
"a,b,c".split(",") # ["a", "b", "c"]
"a b c".split()
",".join(["a", "b"]) # "a,b"split() sin argumento divide por espacios y descarta vacíos. join vuelve a unir las partes.
s.strip()
s.lower()
s.upper()
s.replace("a", "b")
s.startswith("ab")
s.endswith(".py")
s.count("a")
s.find("a") # -1Las cadenas son inmutables: los métodos devuelven una nueva. find da -1 si no encuentra; index lanza error.
s.isdigit()
s.isalpha()
s.isalnum()
c.isupper()
c.islower()
"ABC123".isupper() # True
"123".isupper() # False
"ab" in sisdigit/isalpha/isalnum son verdaderos solo si la cadena no está vacía y coincide entera. isupper/islower comprueban todos los caracteres con mayúsculas y minúsculas: los dígitos y signos se ignoran, pero hace falta al menos uno de ellos. `in` comprueba si un carácter o subcadena está presente.
sorted(s)
sorted(s1) == sorted(s2)
set("aab") == set("ab") # True
sorted("aab") == sorted("ab") # Falsesorted(s) devuelve los caracteres ordenados, así que comparar dos de esas listas es la comprobación típica de anagrama. Un set no sirve: pierde cuántas veces se repite cada carácter. Ambas formas distinguen mayúsculas, espacios y signos.
parts = []
for w in words:
parts.append(w)
result = "".join(parts)No construyas cadenas con += en un bucle: cada paso copia todo. Acumula en una lista y une al final.
ord('a') # 97
chr(97) # 'a'
ord(c) - ord('a')Código del carácter y vuelta. ord(c) - ord("a") da el índice de la letra — típico para contadores de 26.
Cadenas: nivel avanzado10
"{name}: {score:.2f}".format(name="Ada", score=9.876)
score = 9.876
f"{score:.2f}"`str.format` sustituye valores en una plantilla, mientras que una f-string evalúa expresiones directamente en el ámbito actual. Los especificadores controlan la precisión, el signo y la representación, y las llaves literales se escriben como `{{` y `}}`.
x = "42"
f"{x:>8}"
f"{x:<8}"
f"{x:*^8}"Los especificadores `<`, `>` y `^` alinean un valor a la izquierda, a la derecha o al centro, y el carácter anterior define el relleno. El ancho es mínimo y no recorta un valor demasiado largo.
table = str.maketrans({"a": "@", "e": "3"})
"peace".translate(table)`str.maketrans` crea una tabla de transformación y `translate` la aplica a todos los caracteres en una sola pasada. Un valor `None` en la tabla elimina el carácter correspondiente.
head, sep, tail = "key=value".partition("=")`partition` divide solo por la primera aparición y siempre devuelve tres elementos, incluido el propio separador. Si no encuentra el separador, la cadena original queda en el primer elemento y los otros dos están vacíos.
"a\nb\r\nc".splitlines() # ["a", "b", "c"]`splitlines` reconoce distintos tipos de saltos de línea y los elimina por defecto. A diferencia de `split("\n")`, gestiona correctamente varios separadores de línea estándar.
"42".zfill(5) # "00042"
"-42".zfill(5) # "-0042"`zfill` completa una cadena con ceros a la izquierda hasta un ancho mínimo y coloca los ceros después del signo numérico. No recorta una cadena que ya sea más larga que el ancho solicitado.
"unhappy".removeprefix("un") # "happy"
"report.csv".removesuffix(".csv") # "report"`removeprefix` y `removesuffix` eliminan una coincidencia exacta solo del extremo correspondiente de la cadena. A diferencia de `strip`, su argumento no se interpreta como un conjunto de caracteres individuales.
import re
re.match(r"\d+", text)
re.search(r"\d+", text)
re.findall(r"\d+", text)
re.sub(r"\s+", " ", text)`match` comprueba solo el inicio de la cadena, `search` encuentra la primera coincidencia en cualquier lugar, `findall` reúne todas las coincidencias y `sub` las sustituye. Un patrón reutilizado suele compilarse una vez con `re.compile`.
data = "café".encode("utf-8")
text = data.decode("utf-8")`encode` convierte un `str` en `bytes`, mientras que `decode` realiza la conversión inversa usando una codificación indicada. Usar codificaciones incompatibles puede provocar un error o corromper el texto.
"Straße".lower() # "straße"
"Straße".casefold() # "strasse"`lower` realiza una conversión normal a minúsculas, mientras que `casefold` aplica un plegado de mayúsculas más agresivo (ß → ss) para comparar sin distinguir mayúsculas. No es normalización Unicode: una `é` precompuesta y `e` con acento combinante siguen siendo distintas tras `casefold` — para eso está `unicodedata.normalize`.
Listas y slices13
a = [0, 1, 2, 3, 4]
a[1:4] # [1, 2, 3]
a[:3]
a[-2:]
a[::-1]
b = a[:]Un slice crea una lista nueva. a[:] copia solo el nivel superior — las listas anidadas se comparten.
a.append(x) # O(1)
a.extend([x, y])
a.insert(0, x) # O(n)
a.pop() # O(1)
a.pop(0) # O(n)Insertar o quitar al principio cuesta O(n). Si necesitas ambos extremos, usa collections.deque.
for i, x in enumerate(a):
...
for i, x in enumerate(a, start=1):
...Índice y valor a la vez. Sustituye a for i in range(len(a)) y evita el error por uno.
for x, y in zip(a, b):
...
list(zip(a, b))
zip(*matrix)Recorre varias secuencias en paralelo y se detiene en la más corta. zip(*matrix) transpone una matriz.
[x for x in a if x > 0]
[x * 2 for x in a]
[y for row in m for y in row]
{x: x ** 2 for x in a}Más corto y rápido que un bucle con append. Dos for aplanan una lista anidada; las llaves con dos puntos crean un diccionario. Los for anidados se leen en el mismo orden que un bucle.
any(x > 0 for x in a)
all(x > 0 for x in a)
sum(a)
min(a)
max(a)
max(a, key=abs)
max(a, default=0)any/all cortan en el primer elemento decisivo. key elige qué comparar; default evita el error con lista vacía.
grid = [[0] * m for _ in range(n)] # ✓
grid = [[0] * m] * n # ✗La segunda forma es incorrecta: multiplicar una lista copia la referencia, no el contenido, y todas las filas serían la misma. Usa comprensión.
a = [1, 2, 3]
b = list(range(3))
zeros = [0] * 5
rows = [[0] * 3] * 2Un literal o `list()` crea una lista, mientras que la multiplicación repite sus elementos. Al repetir un objeto mutable anidado se copian referencias, por lo que las filas de `rows` quedan vinculadas.
a = [4, 2, 4]
i = a.index(4)
n = a.count(4)`index()` devuelve la posición de la primera coincidencia, mientras que `count()` devuelve el número de coincidencias. `index()` lanza `ValueError` si el valor no existe.
a = [10, 20, 30, 20]
a.remove(20)
del a[0]
x = a.pop()`remove()` elimina el primer valor coincidente, `del` elimina por índice o corte y `pop()` elimina y devuelve un elemento. Un valor ausente en `remove()` o un índice no válido provoca una excepción.
from copy import deepcopy
a = [[1], [2]]
b = a.copy()
c = deepcopy(a)
a[0].append(9)`copy()` crea una nueva lista exterior, pero conserva las referencias a los objetos anidados. `deepcopy()` copia recursivamente el contenido anidado, aunque suele requerir más tiempo y memoria.
def add(a, b):
return a + b
nums = [2, 3]
result = add(*nums)El operador `*` pasa los elementos de la lista como argumentos posicionales separados. Su cantidad debe coincidir con la firma de la función, salvo que esta acepte `*args`.
point = (2, 3)
seen = {point}Una tupla se parece a una lista, pero sus elementos no pueden sustituirse, añadirse ni eliminarse. Puede guardarse en un conjunto o usarse como clave de diccionario solo si todos sus elementos son hashables.
Diccionarios y conjuntos13
d[k] # KeyError
d.get(k) # None
d.get(k, 0)
d.setdefault(k, []).append(x)get no lanza excepción. setdefault crea el valor si falta la clave y lo devuelve en un paso.
for k in d:
...
for k, v in d.items():
...
d.keys()
d.values()Iterar un dict devuelve claves. El orden de inserción se conserva desde Python 3.7.
from collections import Counter
c = Counter(a)
c[x] # 0
c.most_common(2)
Counter(s1) == Counter(s2)Un contador de frecuencias listo. Una clave ausente da 0, no error; most_common devuelve los más frecuentes.
from collections import defaultdict
g = defaultdict(list)
g[k].append(x)
cnt = defaultdict(int)
cnt[k] += 1El valor se crea al primer acceso. Útil para agrupar y para listas de adyacencia de grafos.
s = set(a)
s.add(x)
s.discard(x)
x in s # O(1)
a_set & b_set
a_set | b_set
a_set - b_setLa pertenencia es O(1) frente a O(n) en listas. & es intersección, | unión, - diferencia. Un set no guarda orden ni duplicados.
seen = set()
for x in a:
if x in seen:
return True
seen.add(x)Encuentra un duplicado en una pasada, O(n). La misma forma resuelve two-sum: busca target - x en seen.
d1 = {"a": 1}
d2 = dict(a=1)
d3 = dict([("a", 1)])
d4 = {k: 0 for k in ("a", "b")}Un diccionario puede crearse con un literal, `dict()`, una secuencia de pares o una comprensión de diccionario. Los argumentos nombrados de `dict()` solo sirven para claves de texto que sean identificadores válidos.
d = {"a": 1, "c": 3}
"a" in d # True
x = d.pop("a", 0)
y = d.pop("b", 0)
del d["c"]En un diccionario, `in` comprueba las claves y no los valores. `del` lanza `KeyError` si la clave no existe, mientras que `pop(key, default)` devuelve el valor alternativo.
d = {"a": 1, "b": 2}
keys = d.keys()
values = d.values()
pairs = d.items()
d["c"] = 3
list(keys) # ["a", "b", "c"]Los métodos `keys()`, `values()` e `items()` devuelven vistas dinámicas, no listas. Reflejan los cambios posteriores del diccionario; para obtener una copia independiente hay que convertirlas explícitamente en `list`.
a = {"x": 1}
b = {"x": 2, "y": 3}
c = a | b
a.update(b)El operador `|` crea un diccionario nuevo, mientras que `update()` modifica el existente. Cuando las claves coinciden, prevalece el valor del diccionario derecho o de la fuente proporcionada.
d = {"a": 3, "b": 1, "c": 2}
pairs = sorted(d.items(), key=lambda item: item[1])Para ordenar por valores, se ordenan los pares de `items()` usando el segundo elemento como clave. El resultado es una lista de tuplas, no un diccionario ordenado automáticamente.
freq = {}
for x in [2, 1, 2]:
freq[x] = freq.get(x, 0) + 1Las frecuencias pueden contarse con un diccionario normal usando cero para una clave todavía no vista. Sin un valor por defecto, `get()` devuelve `None`, que no puede sumarse a un número.
edge = frozenset({2, 5})
weights = {edge: 7}Un `frozenset` es inmutable y hashable, por lo que puede usarse como clave de diccionario o como elemento de otro conjunto. Un `set` normal no puede utilizarse de esta forma.
Colecciones de la biblioteca estándar10
from collections import deque
q = deque([2, 3])
q.appendleft(1)
q.append(4)
left = q.popleft()
right = q.pop()deque permite añadir y eliminar elementos por ambos extremos en O(1). Para una cola, usa append y popleft en vez de pop(0) de una lista.
from collections import deque
q = deque([1, 2, 3], maxlen=3)
q.append(4)
q.rotate(1)
q.rotate(-1)maxlen elimina automáticamente elementos del extremo opuesto cuando se llena el deque. rotate desplaza a la derecha con un argumento positivo y a la izquierda con uno negativo.
import heapq
h = [4, 1, 7, 2]
heapq.heapify(h)
heapq.heappush(h, 0)
smallest = heapq.heappop(h)heapq implementa un montículo mínimo, por lo que el menor elemento siempre está en la raíz. heapify funciona en O(n), mientras que push y pop requieren O(log n).
import heapq
nums = [4, 1, 7, 2]
largest = heapq.nlargest(2, nums)
smallest = heapq.nsmallest(2, nums)
h = [-x for x in nums]
heapq.heapify(h)
maximum = -heapq.heappop(h)nlargest y nsmallest permiten elegir los k elementos extremos sin ordenar toda la entrada. Un montículo máximo suele simularse guardando los números con signo contrario.
from collections import Counter
a = Counter("banana")
a.update("band")
top = a.most_common(2)
b = Counter("an")
added = a + b
common = a & bCounter almacena frecuencias y admite most_common, update y operaciones entre contadores. Las operaciones aritméticas crean un Counter nuevo y normalmente omiten resultados cero o negativos.
from collections import defaultdict
groups = defaultdict(list)
counts = defaultdict(int)
seen = defaultdict(set)
groups["a"].append(1)
value = counts["missing"]defaultdict crea un valor mediante su fábrica al acceder a una clave ausente. Leer con corchetes modifica el diccionario, así que usa in o get si solo quieres comprobarla.
from collections import OrderedDict
d = {"a": 1, "b": 2}
od = OrderedDict(d)
od.move_to_end("a")
first = od.popitem(last=False)El dict normal ya conserva el orden de inserción, por lo que OrderedDict resulta útil principalmente para reordenar y eliminar desde un extremo concreto. La igualdad entre dos OrderedDict también considera el orden.
from collections import namedtuple
Point = namedtuple("Point", ["x", "y"])
p = Point(2, 3)
x = p.xnamedtuple crea un tipo compacto e inmutable con acceso a los campos por nombre e índice. Para modelos más complejos con métodos y valores predeterminados suele ser más cómodo usar dataclass.
from array import array
nums = array("i", [1, 2, 3])
raw = bytes([65, 66, 67])
mutable = bytearray(raw)
mutable[0] = 90array almacena valores de un único tipo primitivo de forma más compacta que una lista, aunque rara vez se necesita en problemas algorítmicos. bytes es inmutable y bytearray permite modificar bytes individuales.
from collections import ChainMap
defaults = {"timeout": 30}
overrides = {"timeout": 10}
config = ChainMap(overrides, defaults)
value = config["timeout"]ChainMap busca en varios diccionarios de izquierda a derecha sin copiarlos ni combinarlos. Las asignaciones modifican únicamente el primer diccionario de forma predeterminada.
Iteración y generadores10
squares_list = [x * x for x in values]
squares_gen = (x * x for x in values)Una comprensión de lista crea inmediatamente toda la lista, mientras que una expresión generadora produce valores bajo demanda. El generador ahorra memoria, pero normalmente solo puede consumirse una vez.
def countdown(n):
while n > 0:
yield n
n -= 1La presencia de yield convierte la función en generadora: la llamada devuelve un generador y el cuerpo se ejecuta al iterarlo. Después de cada yield, la ejecución continúa desde la posición guardada.
def flatten(chunks):
for chunk in chunks:
yield from chunkyield from transmite los elementos de un iterable anidado sin escribir manualmente un bucle interior. Cada uso aplana solo un nivel.
it = iter([10])
first = next(it)
missing = next(it, None)
try:
next(it)
except StopIteration:
...iter obtiene un iterador y next extrae el siguiente elemento avanzando su estado. Proporcionar un valor predeterminado evita StopIteration cuando el iterador se agota.
from itertools import count, cycle, islice
numbers = list(islice(count(10, 2), 4))
pattern = list(islice(cycle("ab"), 5))count y cycle crean iteradores potencialmente infinitos, mientras que islice los limita sin materializar toda la secuencia. islice no admite índices negativos ni un paso negativo.
from itertools import chain
merged = list(chain([1, 2], [3, 4]))
flat = list(chain.from_iterable([[1, 2], [3, 4]]))chain recorre de forma perezosa varios iterables uno tras otro. chain.from_iterable recibe un iterable exterior y aplana un nivel.
from itertools import combinations, permutations, product
pairs = list(product([0, 1], repeat=2))
orders = list(permutations([1, 2, 3], 2))
choices = list(combinations([1, 2, 3], 2))product construye un producto cartesiano, permutations tiene en cuenta el orden y combinations no. Devuelven iteradores, pero el número de resultados puede crecer de forma combinatoria.
from itertools import groupby
items = [1, 1, 2, 2, 2, 3]
groups = [(key, list(group)) for key, group in groupby(items)]groupby agrupa únicamente elementos consecutivos con la misma clave. Para una agrupación global, normalmente hay que ordenar antes por esa misma clave.
a = [1, 2, 3]
it = reversed(a)
b = list(it)reversed devuelve un iterador inverso y no modifica la secuencia original. El objeto debe admitir iteración inversa o proporcionar acceso por índice y longitud.
total = sum(x * x for x in values)
has_negative = any(x < 0 for x in values)
all_even = all(x % 2 == 0 for x in values)sum consume todos los valores del generador, mientras que any y all se detienen en cuanto se conoce el resultado. Con una fuente vacía, any devuelve False y all devuelve True.
Números y matemáticas10
n = 10**100
n.bit_length()El tipo `int` almacena enteros de tamaño arbitrario, por lo que no existe desbordamiento de ancho fijo. Los límites prácticos son la memoria y el tiempo de cálculo, pero desde Python 3.11 la conversión entre `int` y `str` está limitada a 4300 dígitos y lanza `ValueError` al superarlos.
0.1 + 0.2 == 0.3 # False
x = float("inf")El tipo `float` usa aproximaciones binarias, por lo que muchas fracciones decimales no pueden representarse exactamente. Usa `math.isclose` para comparar y `decimal.Decimal` cuando necesites aritmética decimal exacta.
round(2.5) # 2
round(3.5) # 4
round(12.345, 2)`round` resuelve un empate exacto hacia el valor par más cercano en vez de redondear siempre hacia arriba. El resultado con valores `float` también puede verse afectado por el error de representación binaria.
abs(-7) # 7
2**10 # 1024
pow(2, 10) # 1024
pow(2, 10, 1000) # 24`abs` devuelve el valor absoluto, mientras que `**` y `pow` realizan la potenciación. La forma `pow(a, b, mod)` calcula eficientemente una potencia modular sin crear un entero intermedio enorme.
from math import sqrt, isqrt, gcd, factorial
sqrt(81) # 9.0
isqrt(80) # 8
gcd(18, 24) # 6
factorial(5) # 120`sqrt` devuelve una raíz de punto flotante, mientras que `isqrt` devuelve la parte entera exacta sin errores de `float`. `gcd` calcula el máximo común divisor y `factorial` solo acepta enteros no negativos.
from math import ceil, floor, inf
ceil(2.1) # 3
floor(2.9) # 2
inf > 10**100 # True`ceil` redondea hacia infinito positivo y `floor` hacia infinito negativo, algo especialmente importante con números negativos. `math.inf` se compara como infinito, pero no aparece automáticamente al dividir entre cero.
try:
1 / 0
except ZeroDivisionError:
...La división, la división entera y el módulo con divisor cero lanzan `ZeroDivisionError`. Python no convierte esa división en infinito.
int("101", 2) # 5
int("0xff", 0) # 255
int(3.9) # 3
float("2.5") # 2.5
str(42) # "42"
bin(10) # "0b1010"
hex(255) # "0xff"
oct(8) # "0o10"`int` puede interpretar cadenas con bases de 2 a 36, mientras que la base `0` detecta automáticamente un prefijo estándar. Convertir un `float` a `int` trunca hacia cero, y `bin`, `hex` y `oct` devuelven cadenas con prefijo.
x = 0b1100
y = 0b1010
x & y # 8
x | y # 14
x ^ y # 6
~x # -13
x << 2 # 48
x >> 2 # 3
x.bit_count() # 2Las operaciones bit a bit trabajan con la representación binaria de los enteros y suelen usarse para máscaras y conjuntos de estados. Los enteros no tienen un ancho fijo y `~x` siempre equivale a `-x - 1`.
import random
rng = random.Random(42)
rng.randrange(1, 10)
rng.randint(1, 10)
rng.choice([1, 2, 3])
a = [1, 2, 3]
rng.shuffle(a)`random` genera valores pseudoaleatorios reproducibles: `randrange` excluye el límite final, mientras que `randint` incluye ambos extremos. El módulo no es adecuado para contraseñas ni criptografía.
Ordenación y búsqueda6
b = sorted(a)
a.sort()
sorted(a, reverse=True)sorted devuelve una lista nueva; sort ordena en el sitio y devuelve None. La ordenación es estable.
sorted(words, key=len)
sorted(pairs, key=lambda p: p[1])
sorted(a, key=lambda x: (-x[1], x[0]))key se llama una vez por elemento. Una tupla da varios niveles; el menos invierte un campo numérico.
a.sort(key=lambda x: x.age)
a.sort(key=lambda x: x.name)Los elementos iguales conservan su orden, así que puedes ordenar primero por la clave menos significativa.
from operator import itemgetter
sorted(rows, key=itemgetter(1, 0))Igual que una lambda pero más rápido, y más claro al ordenar por varios campos.
from bisect import bisect_left, bisect_right, insort
i = bisect_left(a, x)
j = bisect_right(a, x)
insort(a, x)Búsqueda binaria en una lista ordenada en O(log n). bisect_left da la primera posición de x y bisect_right la siguiente, así que j - i es el número de apariciones. insort inserta manteniendo el orden.
lo, hi = 0, len(a) - 1
while lo <= hi:
mid = (lo + hi) // 2
if a[mid] == x:
return mid
if a[mid] < x:
lo = mid + 1
else:
hi = mid - 1
return -1La lista debe estar ordenada. Mueve los límites más allá de mid o el bucle no termina.
Pila, cola y dos punteros9
stack = []
stack.append(1)
stack.append(2)
top = stack.pop()Una lista implementa eficazmente una pila LIFO con append y pop desde el final en O(1) amortizado. Eliminar desde el inicio de una lista requiere O(n).
from collections import deque
q = deque([1, 2])
q.append(3)
first = q.popleft()Una cola FIFO se implementa eficazmente con deque: append añade a la derecha y popleft elimina a la izquierda en O(1). Una lista con pop(0) no es adecuada para colas grandes porque desplaza los elementos.
def next_greater(nums):
result = [-1] * len(nums)
stack = []
for i, x in enumerate(nums):
while stack and nums[stack[-1]] < x:
result[stack.pop()] = x
stack.append(i)
return resultUna pila monótona mantiene valores o índices ordenados dentro de la pila y encuentra el elemento mayor o menor más cercano en O(n). Conviene guardar índices cuando se necesitan distancias o posiciones.
def is_balanced(s):
pairs = {")": "(", "]": "[", "}": "{"}
stack = []
for ch in s:
if ch in "([{":
stack.append(ch)
elif ch in pairs and (not stack or stack.pop() != pairs[ch]):
return False
return not stackLos símbolos de apertura se guardan en una pila y cada cierre debe coincidir con la cima. Al terminar, la pila debe estar vacía o quedarán símbolos sin cerrar.
def has_pair(nums, target):
left, right = 0, len(nums) - 1
while left < right:
total = nums[left] + nums[right]
if total == target:
return True
if total < target:
left += 1
else:
right -= 1
return FalseLos punteros desde extremos opuestos reducen el espacio de búsqueda cuando el resultado actual indica cuál debe moverse. Para buscar una pareja con una suma dada, el array debe estar ordenado.
def middle_node(head):
slow = fast = head
while fast and fast.next:
slow = slow.next
fast = fast.next.next
return slowUn puntero rápido avanza más que uno lento, lo que permite encontrar el centro o detectar ciclos sin memoria adicional. Antes del salto doble hay que comprobar fast y su nodo siguiente.
def remove_duplicates(nums):
if not nums:
return 0
write = 1
for read in range(1, len(nums)):
if nums[read] != nums[write - 1]:
nums[write] = nums[read]
write += 1
return writeUn puntero de lectura recorre el array y uno de escritura construye el prefijo válido sin otro array. Esta versión supone que los valores iguales están juntos, normalmente porque la entrada está ordenada.
def reverse_in_place(nums):
left, right = 0, len(nums) - 1
while left < right:
nums[left], nums[right] = nums[right], nums[left]
left += 1
right -= 1Intercambiar elementos desde ambos extremos invierte una secuencia mutable en O(n) y con O(1) de memoria adicional. Una cadena no puede modificarse así porque es inmutable.
def merge_sorted(a, b):
i = j = 0
result = []
while i < len(a) and j < len(b):
if a[i] <= b[j]:
result.append(a[i])
i += 1
else:
result.append(b[j])
j += 1
result.extend(a[i:])
result.extend(b[j:])
return resultDos punteros fusionan secuencias ordenadas en O(n + m), eligiendo cada vez el menor elemento actual. Después del bucle principal hay que añadir la parte restante.
Ventana deslizante y sumas de prefijos9
def window_sums(nums, k):
if k > len(nums):
return []
current = sum(nums[:k])
result = [current]
for right in range(k, len(nums)):
current += nums[right] - nums[right - k]
result.append(current)
return resultUna ventana fija actualiza su estado añadiendo el elemento que entra y eliminando el que sale, por lo que procesa todas las ventanas en O(n). Hay que definir el comportamiento cuando k es cero o supera la longitud.
def min_length(nums, target):
left = 0
total = 0
answer = len(nums) + 1
for right, x in enumerate(nums):
total += x
while total >= target:
answer = min(answer, right - left + 1)
total -= nums[left]
left += 1
return 0 if answer > len(nums) else answerUna ventana variable se amplía por la derecha y se contrae por la izquierda mientras se cumple la condición. Las versiones basadas en sumas suelen requerir valores no negativos para conservar la monotonicidad.
from collections import Counter
def anagram_windows(s, pattern):
need = Counter(pattern)
window = Counter()
result = []
k = len(pattern)
for right, ch in enumerate(s):
window[ch] += 1
if right >= k:
left_ch = s[right - k]
window[left_ch] -= 1
if window[left_ch] == 0:
del window[left_ch]
if window == need:
result.append(right - k + 1)
return resultLas frecuencias de la ventana se actualizan solo para el elemento que entra y el que sale, evitando recálculos. Conviene eliminar los conteos cero para que las comparaciones sean más claras.
def max_window_sum(nums, k):
current = sum(nums[:k])
best = current
for right in range(k, len(nums)):
current += nums[right] - nums[right - k]
best = max(best, current)
return bestLa suma de la siguiente ventana se obtiene restando el elemento izquierdo y añadiendo el nuevo elemento derecho. Si puede haber valores negativos, inicializa el máximo con la primera ventana completa y no con cero.
def range_sum(prefix, left, right):
return prefix[right] - prefix[left]
nums = [2, 4, 1]
prefix = [0]
for x in nums:
prefix.append(prefix[-1] + x)
value = range_sum(prefix, 1, 3)Un array de prefijos con un cero inicial permite obtener la suma del intervalo semiabierto [left, right) en O(1). El cero adicional simplifica los límites y los rangos que comienzan en el primer elemento.
nums = [3, 8, 10, 15]
diffs = [b - a for a, b in zip(nums, nums[1:])]Las diferencias adyacentes describen los cambios entre posiciones y permiten reconstruir valores mediante acumulación. El resultado contiene un elemento menos que la secuencia original.
from itertools import accumulate
prefix = list(accumulate([2, 4, 1], initial=0))
products = list(accumulate([2, 3, 4], lambda a, b: a * b))accumulate produce de forma perezosa los resultados intermedios y acepta una función binaria personalizada. initial añade un valor inicial a la salida y cambia su longitud.
def has_subarray_sum(nums, target):
seen = {0}
prefix = 0
for x in nums:
prefix += x
if prefix - target in seen:
return True
seen.add(prefix)
return FalseUn subarray suma target cuando dos sumas de prefijo difieren en target. Un conjunto de prefijos permite O(n) y funciona con valores negativos, a diferencia de una ventana deslizante basada en suma.
def first_true(low, high, feasible):
while low < high:
mid = (low + high) // 2
if feasible(mid):
high = mid
else:
low = mid + 1
return lowLa búsqueda binaria sobre la respuesta encuentra el límite donde un predicado monótono cambia de False a True. La dificultad principal es elegir límites inclusivos coherentes y demostrar la monotonicidad de feasible.
Recursión y programación dinámica10
def factorial(n):
if n <= 1:
return 1
return n * factorial(n - 1)El caso base detiene la recursión y el paso recursivo reduce el problema a una instancia menor. Sin un caso base alcanzable, las llamadas terminan con RecursionError.
import sys
sys.setrecursionlimit(200000)Python limita la profundidad de la pila y lanza RecursionError cuando se supera. El límite debe aumentarse con cuidado porque una recursión muy profunda puede agotar la pila del sistema.
def factorial_tail(n, acc=1):
if n <= 1:
return acc
return factorial_tail(n - 1, acc * n)Una llamada de cola es la última operación de una función, pero Python no optimiza estas llamadas. La recursión de cola sigue consumiendo pila y normalmente se sustituye por un bucle.
from functools import cache, lru_cache
@lru_cache(maxsize=None)
def fib_lru(n):
return n if n < 2 else fib_lru(n - 1) + fib_lru(n - 2)
@cache
def fib_cache(n):
return n if n < 2 else fib_cache(n - 1) + fib_cache(n - 2)Estos decoradores guardan los resultados según los argumentos y eliminan cálculos repetidos. cache equivale a un lru_cache sin límite y todos los argumentos deben ser hashables.
def fib(n):
if n < 2:
return n
dp = [0] * (n + 1)
dp[1] = 1
for i in range(2, n + 1):
dp[i] = dp[i - 1] + dp[i - 2]
return dp[n]La DP ascendente calcula los estados desde los más simples hasta los más complejos y los guarda en un array. Evita la pila de recursión, aunque a veces el array puede reducirse a unas pocas variables.
def grid_paths(rows, cols):
dp = [[0] * cols for _ in range(rows)]
for r in range(rows):
dp[r][0] = 1
for c in range(cols):
dp[0][c] = 1
for r in range(1, rows):
for c in range(1, cols):
dp[r][c] = dp[r - 1][c] + dp[r][c - 1]
return dp[-1][-1]Se usa una tabla bidimensional cuando un estado depende de dos parámetros, como una posición de fila y columna. Es importante inicializar correctamente los bordes antes de rellenar las demás celdas.
def min_coins(coins, amount):
inf = amount + 1
dp = [0] + [inf] * amount
prev = [-1] * (amount + 1)
for total in range(1, amount + 1):
for coin in coins:
if coin <= total and dp[total - coin] + 1 < dp[total]:
dp[total] = dp[total - coin] + 1
prev[total] = coin
if dp[amount] == inf:
return None
answer = []
while amount > 0:
coin = prev[amount]
answer.append(coin)
amount -= coin
return answerAdemás del valor óptimo, la DP puede guardar la elección usada para alcanzar cada estado. Después, la respuesta se reconstruye recorriendo hacia atrás desde el estado final.
def merge_sort(a):
if len(a) <= 1:
return a
mid = len(a) // 2
left = merge_sort(a[:mid])
right = merge_sort(a[mid:])
result = []
i = j = 0
while i < len(left) and j < len(right):
if left[i] <= right[j]:
result.append(left[i])
i += 1
else:
result.append(right[j])
j += 1
return result + left[i:] + right[j:]El problema se divide en subproblemas independientes cuyas soluciones recursivas se combinan después. La eficiencia depende de que la división esté equilibrada y del coste de combinar los resultados.
def permutations(a):
result = []
def backtrack(start):
if start == len(a):
result.append(a.copy())
return
for i in range(start, len(a)):
a[start], a[i] = a[i], a[start]
backtrack(start + 1)
a[start], a[i] = a[i], a[start]
backtrack(0)
return resultEl backtracking elige una opción, continúa recursivamente y después restaura el estado modificado. Generar todas las permutaciones requiere tiempo factorial.
def subsets(a):
result = []
current = []
def backtrack(index):
if index == len(a):
result.append(current.copy())
return
backtrack(index + 1)
current.append(a[index])
backtrack(index + 1)
current.pop()
backtrack(0)
return resultPara cada elemento, la recursión considera dos opciones: incluirlo u omitirlo. El resultado actual debe copiarse al guardarlo y restaurarse después de la llamada recursiva.
Grafos y árboles10
edges = [(0, 1), (0, 2), (1, 2)]
graph = {0: [], 1: [], 2: []}
for u, v in edges:
graph[u].append(v)
graph[v].append(u)Una lista de adyacencia guarda los vecinos de cada vértice y normalmente requiere O(V + E) de memoria. En un grafo dirigido no se debe añadir automáticamente la arista inversa.
from collections import deque
def bfs(graph, start):
queue = deque([start])
visited = {start}
order = []
while queue:
node = queue.popleft()
order.append(node)
for neighbor in graph[node]:
if neighbor not in visited:
visited.add(neighbor)
queue.append(neighbor)
return orderBFS recorre un grafo por niveles mediante una cola y encuentra distancias mínimas en un grafo no ponderado. Conviene marcar un vértice al añadirlo a la cola para evitar inserciones duplicadas.
def dfs_recursive(graph, start):
visited = set()
order = []
def dfs(node):
visited.add(node)
order.append(node)
for neighbor in graph[node]:
if neighbor not in visited:
dfs(neighbor)
dfs(start)
return orderEl DFS recursivo explora por completo una rama antes de pasar a la siguiente. En cadenas largas puede superar el límite de recursión de Python.
def dfs_iterative(graph, start):
stack = [start]
visited = set()
order = []
while stack:
node = stack.pop()
if node in visited:
continue
visited.add(node)
order.append(node)
stack.extend(reversed(graph[node]))
return orderEl DFS iterativo usa una pila explícita y no depende del límite de recursión. El orden del recorrido depende del orden en que se añaden los vecinos a la pila.
from collections import deque
def shortest_path(graph, start, target):
queue = deque([start])
parent = {start: None}
while queue:
node = queue.popleft()
if node == target:
break
for neighbor in graph[node]:
if neighbor not in parent:
parent[neighbor] = node
queue.append(neighbor)
if target not in parent:
return None
path = []
node = target
while node is not None:
path.append(node)
node = parent[node]
return path[::-1]BFS alcanza por primera vez un vértice mediante un camino con el número mínimo de aristas. Para reconstruir el camino basta con guardar el padre de cada vértice visitado por primera vez.
def preorder(node, result):
if node is None:
return
result.append(node.val)
preorder(node.left, result)
preorder(node.right, result)
def inorder(node, result):
if node is None:
return
inorder(node.left, result)
result.append(node.val)
inorder(node.right, result)
def postorder(node, result):
if node is None:
return
postorder(node.left, result)
postorder(node.right, result)
result.append(node.val)Preorder procesa el nodo antes que sus hijos, inorder entre los subárboles izquierdo y derecho, y postorder después de los hijos. Inorder produce un orden ordenado solo en un árbol binario de búsqueda válido.
from collections import deque
def level_order(root):
if root is None:
return []
queue = deque([root])
levels = []
while queue:
level = []
for _ in range(len(queue)):
node = queue.popleft()
level.append(node.val)
if node.left is not None:
queue.append(node.left)
if node.right is not None:
queue.append(node.right)
levels.append(level)
return levelsEl recorrido por niveles es un BFS aplicado a un árbol y utiliza una cola. El tamaño de la cola al inicio de una iteración fija el número de nodos del nivel actual.
def has_cycle(graph):
state = {}
def dfs(node):
state[node] = 1
for neighbor in graph[node]:
if state.get(neighbor, 0) == 1:
return True
if state.get(neighbor, 0) == 0 and dfs(neighbor):
return True
state[node] = 2
return False
return any(state.get(node, 0) == 0 and dfs(node) for node in graph)En un grafo dirigido, una arista hacia un vértice que está en la pila DFS indica un ciclo. Se suelen usar tres estados: no visitado, en procesamiento y completamente procesado.
from collections import deque
def topological_sort(graph):
indegree = {node: 0 for node in graph}
for node in graph:
for neighbor in graph[node]:
indegree[neighbor] += 1
queue = deque(node for node in graph if indegree[node] == 0)
order = []
while queue:
node = queue.popleft()
order.append(node)
for neighbor in graph[node]:
indegree[neighbor] -= 1
if indegree[neighbor] == 0:
queue.append(neighbor)
return order if len(order) == len(graph) else NoneEl algoritmo de Kahn elimina repetidamente los vértices con grado de entrada cero. Si no se procesan todos los vértices, el grafo contiene un ciclo y no existe un orden topológico.
class DSU:
def __init__(self, n):
self.parent = list(range(n))
self.size = [1] * n
def find(self, x):
while x != self.parent[x]:
self.parent[x] = self.parent[self.parent[x]]
x = self.parent[x]
return x
def union(self, a, b):
a = self.find(a)
b = self.find(b)
if a == b:
return False
if self.size[a] < self.size[b]:
a, b = b, a
self.parent[b] = a
self.size[a] += self.size[b]
return TrueUnion-Find comprueba rápidamente si dos vértices pertenecen a la misma componente y une componentes. La compresión de caminos y la unión por tamaño ofrecen un tiempo amortizado casi constante.
Excepciones10
try:
value = int(text)
except ValueError:
value = 0
else:
value += 1
finally:
closed = Trueexcept se ejecuta tras una excepción, else solo si try termina correctamente y finally se ejecuta siempre. Conviene incluir en try únicamente las operaciones cuyas excepciones se esperan.
try:
item = data[key]
except KeyError:
item = NoneCaptura un tipo de excepción concreto para no ocultar errores ajenos. Un except sin tipo solo es apropiado en casos poco frecuentes donde la excepción se gestiona correctamente o se vuelve a lanzar.
try:
result = values[index] / divisor
except (IndexError, ZeroDivisionError):
result = NoneVarios tipos de excepción con el mismo tratamiento pueden agruparse en una tupla dentro de un solo except. Si el tratamiento es distinto, deben usarse bloques except separados.
def load_number(text):
try:
return int(text)
except ValueError as exc:
raise ValueError("invalid number") from exc
def parse_number(text):
try:
return load_number(text)
except ValueError:
raiseraise crea una excepción, mientras que raise sin argumentos vuelve a lanzar la actual. La forma raise ... from ... conserva una cadena causal explícita entre excepciones.
class InvalidStateError(Exception):
pass
def require_ready(ready):
if not ready:
raise InvalidStateError("not ready")Las excepciones propias heredan de Exception y permiten distinguir errores del dominio de fallos integrados. A menudo basta con una clase vacía con un nombre claro.
def built_in_errors(data, key, index, value, divisor):
if not isinstance(value, int):
raise TypeError
if divisor == 0:
raise ZeroDivisionError
if key not in data:
raise KeyError(key)
if not -len(data) <= index < len(data):
raise IndexError(index)
if value < 0:
raise ValueError(value)ValueError indica un valor no válido de un tipo aceptable y TypeError un tipo no válido; KeyError e IndexError representan claves o índices inexistentes. ZeroDivisionError aparece al dividir o calcular el resto entre cero.
def divide(a, b):
assert b != 0
return a / bassert comprueba invariantes internos y lanza AssertionError cuando no se cumplen. Las comprobaciones se eliminan al ejecutar Python con -O, por lo que no deben validar datos del usuario.
with open("data.txt", encoding="utf-8") as file:
text = file.read()Un gestor de contexto garantiza la liberación del recurso tanto al finalizar normalmente como al producirse una excepción. En los archivos, esto implica el cierre automático al salir del bloque with.
from contextlib import suppress
from pathlib import Path
path = Path("data.txt")
with suppress(FileNotFoundError):
path.unlink()contextlib.suppress ignora únicamente los tipos de excepción indicados y resulta útil para casos breves y esperados. No debe usarse para ocultar errores que requieren tratamiento o registro.
def eafp(data, key):
try:
return data[key]
except KeyError:
return None
def lbyl(data, key):
if key in data:
return data[key]
return NoneEAFP realiza primero la operación y gestiona el fallo esperado, mientras que LBYL comprueba antes una condición. EAFP suele ser más idiomático en Python y evita diferencias entre la comprobación y la operación.
Clases y objetos10
class Point:
def __init__(self, x, y):
self.x = x
self.y = y
def move(self, dx, dy):
self.x += dx
self.y += dyUna clase describe la estructura de los objetos y __init__ inicializa cada instancia nueva. self se pasa automáticamente y representa el objeto actual.
class Counter:
total = 0
def __init__(self):
self.value = 0
Counter.total += 1Un atributo de clase se comparte entre todas las instancias, mientras que un atributo de self pertenece a un objeto. Modificar un atributo de clase mutable puede afectar inesperadamente a todas las instancias.
class Account:
def __init__(self, balance):
self.balance = balance
@property
def balance(self):
return self._balance
@balance.setter
def balance(self, value):
if value < 0:
raise ValueError
self._balance = value@property permite usar la lógica de un método mediante el acceso normal a un atributo y validar lecturas o escrituras. Normalmente guarda el dato con otro nombre para evitar una recursión infinita.
class Temperature:
def __init__(self, value):
self.value = value
@staticmethod
def is_valid(value):
return value >= -273.15
@classmethod
def zero(cls):
return cls(0)@staticmethod no recibe ni una instancia ni la clase, mientras que @classmethod recibe la clase mediante cls. Los métodos de clase sirven como constructores alternativos y respetan las subclases.
class Base:
def __init__(self, value):
self.value = value
class Child(Base):
def __init__(self, value, extra):
super().__init__(value)
self.extra = extraUna subclase hereda el comportamiento de su clase base y puede ampliarlo o sobrescribirlo. super() llama a la siguiente implementación según el orden de resolución de métodos, algo especialmente importante con herencia múltiple.
class Point:
def __init__(self, x, y):
self.x = x
self.y = y
def __repr__(self):
return f'Point(x={self.x}, y={self.y})'
def __str__(self):
return f'{self.x}, {self.y}'__repr__ está pensado para desarrolladores y depuración, mientras que __str__ ofrece una salida legible para el usuario. Si no existe __str__, str() utiliza __repr__.
class User:
def __init__(self, user_id):
self.user_id = user_id
def __eq__(self, other):
return isinstance(other, User) and self.user_id == other.user_id
def __hash__(self):
return hash(self.user_id)__eq__ define la igualdad lógica y __hash__ permite usar el objeto en conjuntos o como clave de diccionario. Los objetos iguales deben tener el mismo hash y los campos utilizados para calcularlo no deben cambiar.
class Item:
def __init__(self, score, name):
self.score = score
self.name = name
def __lt__(self, other):
return (self.score, self.name) < (other.score, other.name)
items = sorted([Item(2, 'b'), Item(1, 'a')])__lt__ define el operador < y permite que sorted() compare objetos directamente. La comparación de tuplas permite expresar cómodamente el criterio principal y los desempates.
from dataclasses import dataclass, field
@dataclass(frozen=True)
class Node:
value: int
tags: tuple[str, ...] = ()
@dataclass
class Basket:
items: list[str] = field(default_factory=list)@dataclass genera métodos como __init__ y __repr__, mientras que frozen=True impide la asignación normal a los campos. Un valor por defecto inmutable se escribe directamente, pero uno mutable como [] lanza ValueError al crear la clase y exige default_factory.
class Point:
__slots__ = ('x', 'y')
def __init__(self, x, y):
self.x = x
self.y = y__slots__ limita los atributos disponibles de una instancia y suele reducir el uso de memoria. Si no se incluye __dict__, no se pueden añadir dinámicamente otros atributos.
Anotaciones de tipos10
count: int = 3
def repeat(text: str, times: int) -> str:
return text * times
count = '3'Las anotaciones describen los tipos esperados para analizadores estáticos, IDE y documentación. Python no las comprueba automáticamente durante la ejecución y permite valores de otro tipo.
items: list[int] = [1, 2]
scores: dict[str, int] = {'a': 1}
point: tuple[int, int] = (2, 3)Las colecciones integradas pueden parametrizarse con los tipos de sus elementos, claves y valores. tuple[int, int] describe una estructura fija, mientras que tuple[int, ...] describe una tupla de longitud variable.
from typing import Optional
left: Optional[int] = None
right: int | None = NoneOptional[int] e int | None indican que el valor puede ser int o None. Optional no convierte un parámetro en opcional; para ello necesita un valor por defecto.
from typing import Union
value_a: Union[int, str] = 1
value_b: int | str = 'x'Union[int, str] e int | str permiten uno de varios tipos. Las uniones demasiado amplias dificultan el estrechamiento de tipos y el tratamiento de todos los casos.
from typing import Any
value: Any = 1
value = 'x'
value = [1, 2]Any desactiva las comprobaciones estáticas de las operaciones sobre un valor y es compatible con cualquier tipo en ambos sentidos. Su uso excesivo oculta errores y elimina gran parte de la utilidad de las anotaciones.
from collections.abc import Callable
def apply(fn: Callable[[int, int], int], a: int, b: int) -> int:
return fn(a, b)Callable[[A, B], R] describe un objeto invocable que recibe dos argumentos y devuelve R. Callable[..., R] se usa cuando no se conoce la lista exacta de parámetros.
from collections.abc import Iterable, Sequence
def total(values: Iterable[int]) -> int:
return sum(values)
def first(values: Sequence[int]) -> int:
return values[0]Iterable garantiza que el objeto puede recorrerse, pero no que admita índices ni varios recorridos. Sequence también proporciona longitud y acceso por índice, por lo que es un contrato más estricto.
from typing import Literal, TypeAlias
UserId: TypeAlias = int
Mode: TypeAlias = Literal['fast', 'safe']TypeAlias asigna un nombre legible a otro tipo, pero no crea un tipo nuevo e incompatible. Literal limita el valor a literales concretos y resulta útil para modos y estados.
from typing import TypedDict
class UserData(TypedDict):
name: str
age: int
user: UserData = {'name': 'a', 'age': 20}TypedDict describe las claves obligatorias y los tipos de valores de un diccionario normal. Durante la ejecución el objeto sigue siendo un dict y la estructura solo la comprueban herramientas estáticas.
from typing import Generic, TypeVar
T = TypeVar('T')
class Box(Generic[T]):
def __init__(self, value: T):
self.value = value
def get(self) -> T:
return self.valueTypeVar relaciona tipos en distintas partes de una firma y Generic crea una clase parametrizada por ese tipo. A diferencia de Any, el analizador conserva el tipo concreto en cada uso.
Pruebas y depuración10
def add(a, b):
return a + b
def test_add():
assert add(2, 3) == 5pytest detecta funciones cuyo nombre empieza por test (python_functions = test* por defecto; test_ por convención) y considera un assert fallido como un fallo del test. Conviene comparar resultados concretos para obtener diferencias útiles en el informe.
import pytest
def test_zero_division():
with pytest.raises(ZeroDivisionError):
1 / 0pytest.raises comprueba que un bloque lance el tipo de excepción esperado. El test falla si no se produce ninguna excepción o si el tipo es distinto.
import pytest
@pytest.mark.parametrize('x, expected', [(1, 1), (2, 4), (3, 9)])
def test_square(x, expected):
assert x * x == expectedparametrize ejecuta una misma función de test para cada conjunto de datos. Los casos siguen siendo independientes, por lo que el informe identifica la entrada concreta que ha fallado.
import pytest
@pytest.fixture
def sample():
return [1, 2, 3]
def test_length(sample):
assert len(sample) == 3Una fixture prepara datos o recursos y se inyecta en el test mediante un parámetro con el mismo nombre. Las fixtures demasiado amplias crean dependencias ocultas y dificultan la comprensión del test.
import pytest
def test_ratio():
assert 0.1 + 0.2 == pytest.approx(0.3)pytest.approx compara números usando una tolerancia absoluta o relativa. La igualdad directa entre float no es fiable porque muchas fracciones decimales no tienen una representación binaria exacta.
import unittest
class MathTest(unittest.TestCase):
def test_add(self):
self.assertEqual(2 + 3, 5)unittest.TestCase es el modelo de pruebas basado en clases de la biblioteca estándar e incluye métodos como assertEqual, setUp y tearDown. Los métodos de prueba deben empezar por el prefijo test (TestLoader.testMethodPrefix) para que el cargador predeterminado los detecte; por convención se escribe test_.
def square(x):
"""
>>> square(3)
9
"""
return x * x
if __name__ == '__main__':
import doctest
doctest.testmod()doctest ejecuta ejemplos incluidos en docstrings y compara su salida con el resultado escrito. Es útil para ejemplos pequeños, pero resulta frágil cuando cambia el formato de la salida.
import logging
value = 3
print(value)
logging.basicConfig(level=logging.DEBUG)
logging.debug('%s', value)print resulta práctico para una comprobación local rápida, mientras que logging admite niveles, formato y destinos configurables. logging suele ser preferible en bibliotecas y aplicaciones de servidor.
from timeit import timeit
elapsed = timeit('sum(range(100))', number=10_000)timeit ejecuta repetidamente un fragmento pequeño y reduce el efecto del ruido aleatorio de la medición. Deben compararse operaciones equivalentes en las mismas condiciones y no basarse en una sola ejecución.
import traceback
try:
1 / 0
except ZeroDivisionError:
traceback.print_exc()traceback.print_exc() muestra la traza activa desde un manejador de excepciones. Capturar un error sin registrarlo ni volver a lanzarlo es peligroso porque oculta la causa original.
Entrada, salida y archivos10
name = input().strip()`input` lee una línea de la entrada estándar, elimina el salto de línea final y siempre devuelve un `str`. Los espacios de los extremos permanecen salvo que se llame a `strip`.
import sys
tokens = sys.stdin.buffer.read().split()`sys.stdin.buffer.read` lee rápidamente toda la entrada como `bytes`, y `split` la divide por cualquier espacio en blanco. Este enfoque es cómodo para problemas algorítmicos, pero conserva toda la entrada en memoria.
import sys
while line := sys.stdin.readline():
line = line.rstrip("\r\n")`sys.stdin.readline` conserva el salto de línea final y devuelve una cadena vacía al llegar al final de la entrada. La lectura línea por línea evita cargar todos los datos en memoria.
import sys
print(a, b, sep=",", end="\n", file=sys.stderr)`sep` define el separador entre argumentos, `end` define el final de la salida y `file` selecciona el flujo de texto de destino. Muchas llamadas individuales a `print` pueden ser más lentas que construir una sola cadena con `join`.
with open("input.txt", "r", encoding="utf-8") as f:
text = f.read()
with open("output.txt", "w", encoding="utf-8") as f:
f.write(text)`with open` garantiza que el archivo se cierre incluso si ocurre una excepción, y una codificación explícita hace que el comportamiento sea portable. El modo `w` vacía el archivo existente antes de escribir.
import json
data = json.loads(text)
text = json.dumps(data, ensure_ascii=False)`json.loads` convierte una cadena JSON en objetos de Python, mientras que `json.dumps` realiza la conversión inversa. JSON no conserva la diferencia entre listas y tuplas, y las claves de los objetos son cadenas después de analizarlas.
import csv
with open("data.csv", newline="", encoding="utf-8") as f:
rows = list(csv.reader(f))
with open("out.csv", "w", newline="", encoding="utf-8") as f:
csv.writer(f).writerows(rows)`csv.reader` y `csv.writer` gestionan correctamente los delimitadores, las comillas y los saltos de línea dentro de los campos. Los archivos CSV deben abrirse con `newline=""`, y los valores leídos permanecen como cadenas por defecto.
from pathlib import Path
path = Path("data") / "input.txt"
text = path.read_text(encoding="utf-8")
path.write_text(text, encoding="utf-8")`pathlib.Path` crea objetos de ruta portables y une sus partes con `/`. `read_text` y `write_text` son cómodos para archivos pequeños, pero procesan todo el contenido de una vez.
import os
token = os.environ.get("API_TOKEN")`os.environ` expone las variables de entorno del proceso como un mapeo mutable de cadenas. El acceso con corchetes lanza `KeyError` si falta la clave, mientras que `get` devuelve `None` o un valor predeterminado.
import sys
script, *args = sys.argv`sys.argv` contiene los argumentos de la línea de comandos como cadenas, y el elemento de índice `0` suele ser el nombre del script ejecutado. No realiza validación ni conversión; para una interfaz estructurada se usa `argparse`.