El slicing básico lo conoces: lista[1:4] te da elementos del índice 1 al 3. Pero la maquinaria detrás es más interesante que eso, y entenderla bien evita bugs sutiles que aparecen justo cuando menos los esperas.
Cuando escribes lista[inicio:fin:paso], Python construye internamente un objeto slice y lo pasa al método __getitem__ de la lista. El resultado es siempre una lista nueva — una copia superficial del segmento original. Superficial significa que los objetos dentro no se copian: si el elemento es un entero inmutable, da igual; si es otra lista o un objeto mutable, tanto la lista original como el slice apuntan al mismo objeto en memoria. Eso lo veremos con detalle más adelante.
El paso negativo no es un truco: es la misma mecánica con una dirección de recorrido invertida. lista[::-1] construye el slice slice(None, None, -1), que significa “desde el final hasta el principio, de uno en uno”. El índice de inicio por defecto cuando el paso es negativo es len(lista) - 1, y el de fin es “antes del índice -1”, que no existe, o sea, hasta el elemento 0 inclusive.
La parte que más sorprende es que el slicing en el lado izquierdo de una asignación es una operación completamente diferente: no devuelve nada, sino que muta la lista original en su lugar, y puede cambiarle la longitud. lista[1:3] = [10, 20, 30] reemplaza 2 elementos con 3, alargando la lista. lista[1:3] = [] elimina esos elementos. Si usas paso en un slice de asignación, el número de elementos de la derecha debe coincidir exactamente con los slots que defines — Python no puede “estirar” los huecos cuando el paso no es 1.
¿Cuándo usas el objeto slice explícito? Cuando el rango del corte es dinámico o lo quieres nombrar para reutilizarlo. En lugar de esparcir literales [2:8:2] por el código, defines evens = slice(2, 8, 2) una vez y lo pasas donde lo necesitas. También es indispensable cuando implementas __getitem__ en tus propias clases para darles semántica de indexación.
from __future__ import annotations
from typing import Any
# ── 1. Slicing básico y copia superficial ──────────────────────────────
matrix = [[1, 2], [3, 4], [5, 6]]
segment = matrix[0:2] # nueva lista, mismos objetos internos
segment[0].append(99) # muta el objeto compartido
print(matrix) # [[1, 2, 99], [3, 4], [5, 6]] — efecto colateral
import copy
deep_segment = copy.deepcopy(matrix[0:2]) # copia independiente
deep_segment[0].append(0)
print(matrix) # sin cambios esta vez
# ── 2. Paso negativo ───────────────────────────────────────────────────
nums = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
print(nums[::-1]) # [9, 8, 7, 6, 5, 4, 3, 2, 1, 0]
print(nums[8:2:-2]) # [8, 6, 4] — desde idx 8 hacia idx 3, de 2 en 2
print(nums[::3]) # [0, 3, 6, 9]
# ── 3. Asignación a un slice ───────────────────────────────────────────
colors = ["red", "green", "blue", "yellow"]
colors[1:3] = ["cyan", "magenta", "white"] # reemplaza 2 elementos con 3
print(colors) # ['red', 'cyan', 'magenta', 'white', 'yellow']
colors[1:4] = [] # elimina 3 elementos
print(colors) # ['red', 'yellow']
# Con paso extendido la longitud derecha debe ser exacta
letters = list("abcdefgh")
letters[::2] = ["A", "C", "E", "G"] # 4 slots, 4 valores: OK
print(letters) # ['A', 'b', 'C', 'd', 'E', 'f', 'G', 'h']
# ── 4. Objeto slice explícito ──────────────────────────────────────────
# En un parser de CSV fijo, cada columna tiene posición conocida
HEADER = slice(0, 1)
AMOUNT = slice(1, 4)
CURRENCY = slice(4, 7)
def parse_fixed(record: list[str]) -> dict[str, Any]:
return {
"header": record[HEADER],
"amount": record[AMOUNT],
"currency": record[CURRENCY],
}
row = list("TXNEUR0420")
print(parse_fixed(row))
# {'header': ['T', 'X'], 'amount': ['N', 'E', 'U'], 'currency': ['R', '0', '4']}
# ── 5. __getitem__ con objetos slice en una clase propia ───────────────
class InfiniteCounter:
"""Secuencia infinita de enteros desde un offset."""
def __init__(self, start: int = 0) -> None:
self.start = start
def __getitem__(self, key: int | slice) -> int | list[int]:
if isinstance(key, slice):
# slice.indices(length) normaliza None y negativos para un
# rango dado; usamos un largo arbitrario grande para índices
# positivos — en una secuencia infinita no hay límite real.
start, stop, step = key.indices(10_000)
return [self.start + i for i in range(start, stop, step)]
if isinstance(key, int):
if key < 0:
raise IndexError("InfiniteCounter no soporta índices negativos")
return self.start + key
raise TypeError(f"Índice inválido: {type(key)}")
counter = InfiniteCounter(start=10)
print(counter[0]) # 10
print(counter[5]) # 15
print(counter[2:8:2]) # [12, 14, 16]
named = slice(0, 5)
print(counter[named]) # [10, 11, 12, 13, 14]
Qué está pasando en cada parte
La copia superficial del bloque 1 es el bug más frecuente con slicing en listas de listas. matrix[0:2] crea una nueva lista que contiene referencias a los mismos dos sublistas. Cuando haces segment[0].append(99), no estás modificando el slice ni la lista externa — estás modificando el objeto [1, 2] que ambas referencia. copy.deepcopy rompe esa conexión recursivamente.
El paso negativo en el bloque 2 se lee así: nums[8:2:-2] empieza en el índice 8 (valor 8), se mueve hacia la izquierda de dos en dos, y se detiene antes de llegar al índice 2 (el fin es exclusivo también con pasos negativos). Resultado: índices 8, 6, 4 → valores 8, 6, 4.
La asignación con paso extendido en letters[::2] = [...] es estricta porque Python reserva exactamente 4 “slots” (índices 0, 2, 4, 6) y necesita exactamente 4 valores para llenarlos. No puede insertarlos ni eliminar huecos porque los slots no son contiguos.
El objeto slice en el bloque 4 hace el código de parsing legible y mantenible. Si el formato cambia, cambias la definición del slice en un solo lugar. Intentar lograr lo mismo con comentarios sobre índices mágicos es una receta para bugs de mantenimiento.
slice.indices(length) en el bloque 5 es la pieza que falta cuando implementas __getitem__. Recibe la longitud lógica de tu secuencia y devuelve una tupla (start, stop, step) ya normalizada: convierte None en los extremos correctos, resuelve índices negativos relativos a esa longitud, y garantiza que el rango no se salga de límites. Sin esto, tendrías que reimplementar toda esa lógica de normalización a mano.
Errores que debes conocer
Error: usar paso extendido en asignación con un número incorrecto de elementos; Python lanza ValueError en tiempo de ejecución porque los slots ya están determinados.
# ❌ Wrong
letters = list("abcdefgh")
letters[::2] = ["A", "C", "E"] # 4 slots, solo 3 valores → ValueError
# ✅ Right
letters[::2] = ["A", "C", "E", "G"] # exactamente 4 valores para 4 slots
Con paso 1 (o sin paso) la restricción no existe porque los elementos son contiguos y Python puede redimensionar la lista. Con paso distinto de 1, el número de elementos debe ser exacto.
Error: asumir que lista[:] es una copia segura cuando la lista contiene objetos mutables.
# ❌ Wrong original = [[1, 2], [3, 4]] copia = original[:] copia[0].append(99) # también muta original[0] # ✅ Right import copy copia = copy.deepcopy(original) copia[0].append(99) # original queda intacto
lista[:] copia la estructura de la lista exterior pero comparte los objetos internos. deepcopy recorre el grafo de objetos completo y crea duplicados independientes.
Error: olvidar el isinstance(key, slice) en __getitem__ y que Python pase un objeto slice sin avisar cuando el usuario usa notación de slicing.
# ❌ Wrong
class Bag:
def __init__(self, items):
self.items = items
def __getitem__(self, idx):
return self.items[idx] # funciona por accidente porque list ya maneja slice
# El problema aparece cuando la lógica es tuya:
class Grid:
def __init__(self, data):
self.data = data
def __getitem__(self, key):
if isinstance(key, int):
return self.data[key]
raise TypeError("solo enteros") # explota en grid[1:3]
# ✅ Right
class Grid:
def __init__(self, data):
self.data = data
def __getitem__(self, key):
if isinstance(key, slice):
return self.data[key]
if isinstance(key, int):
return self.data[key]
raise TypeError(f"tipo de índice no soportado: {type(key)}")
Cuando el usuario escribe grid[1:3], Python no lanza error antes de llamar a __getitem__ — simplemente construye el slice(1, 3, None) y lo pasa. La responsabilidad de manejarlo es completamente tuya.
N° 41