Listas: creación, métodos y operaciones

Contenido

Imagina que te enfrentas a un problema del mundo real donde debes leer, almacenar, procesar y finalmente imprimir docenas, cientos o miles de números. ¿Qué harías? ¿Crearías una variable independiente para cada valor de la siguiente manera?

var1 = int(input())
var2 = int(input())
var3 = int(input())
var4 = int(input())
var5 = int(input())
var6 = int(input())
# ... y así hasta el infinito

Si crees que esto no es tan complicado, intenta tomar un trozo de papel y diseña la lógica para un programa que reciba solo cinco números y los ordene de menor a mayor (en programación, este proceso se conoce como ordenamiento o sorting). Te darás cuenta rápidamente de que la cantidad de combinaciones de if-else manuales que necesitarías harían que te falte papel antes de terminar.

Variables Escalares vs. Contenedores

Hasta ahora, has aprendido a declarar variables capaces de almacenar un único valor a la vez. En programación y matemáticas, a estas variables simples se les conoce como escalares. Todas las variables que hemos usado hasta este punto (c0, counter, largest_number) son escalares.

La solución al problema del almacenamiento masivo es crear una variable que actúe como un contenedor: una variable única, pero sumamente ancha y capaz de albergar miles de valores independientes.

¿Cómo controlamos un contenedor lleno de datos diferentes? Numerándolos. Si podemos asignarle un número de orden a cada elemento, podemos pedirle a Python cosas como: “dame el valor número 2”, “cambia el valor número 15” o “incrementa el valor número 10,000”.

Tu primera lista en Python

Vamos a crear un contenedor llamado numbers. En lugar de asignarle un solo dígito, lo cargamos con una estructura que encierra cinco valores.

numbers = [10, 5, 7, 2, 1]

Utilizando la terminología adecuada para la certificación PCEP:

  • numbers es una lista que consta de cinco valores.
  • Decimos que esta instrucción crea una lista con una longitud (length) igual a 5 (ya que contiene cinco elementos en su interior).
  • Sintaxis: Las listas en Python se delimitan estrictamente abriendo y cerrando corchetes cuadrados [], y los elementos espaciados en su interior se separan mediante comas ,.

💡 Regla de Tipos para el Test: Una lista en Python es heterogénea. Esto significa que los elementos dentro de una misma lista pueden tener tipos de datos completamente diferentes. Una sola lista puede contener un entero, un flotante, una cadena de texto e incluso otra lista anidada en su interior.

El Sistema de Indexación Base Cero (Crucial)

Python utiliza una convención estándar en el mundo del software: los elementos de una lista siempre se enumeran empezando desde el cero (0). Este número de posición se conoce como índice (index).

Si analizamos nuestra lista numbers = [10, 5, 7, 2, 1]:

  • El primer elemento (10) está en la posición o índice 0.
  • El segundo elemento (5) está en el índice 1.
  • El tercer elemento (7) está en el índice 2.
  • El cuarto elemento (2) está en el índice 3.
  • El quinto y último elemento (1) está en el índice 4.

⚠️ La regla de los límites: Si una lista tiene una longitud de N elementos, sus índices válidos van desde 0 hasta N – 1. En nuestra lista de 5 elementos, el último índice posible es el 4. Intentar acceder al índice 5 romperá el programa inmediatamente.

Para cerrar esta introducción, grábate esta definición técnica: Nuestra lista es una colección de elementos, pero cada elemento individual guardado dentro de ella opera como un escalar.

Operaciones con Listas: Indexación y Reasignación

Para interactuar con un elemento específico dentro de una lista, tanto para leer su valor como para modificarlo, utilizamos una operación llamada indexación (indexing). Sintácticamente, la indexación consiste en colocar el nombre de la lista seguido de corchetes cuadrados [] que encierran el número de posición (el índice) al que queremos apuntar.

nombre_de_la_lista[indice]

Modificar el valor de un elemento

Una de las características más importantes de las listas en Python es que son mutables (pueden cambiar sus elementos internos sobre la marcha sin necesidad de crear una lista nueva). Si queremos cambiar el valor del primerísimo elemento (índice 0) y asignarle un 111, usamos el operador de asignación = de la siguiente manera:

numbers = [10, 5, 7, 2, 1]
print("Contenido original de la lista:", numbers)

# Reasignamos el valor en el índice 0
numbers[0] = 111
print("Nuevo contenido de la lista:", numbers)
Contenido original de la lista: [10, 5, 7, 2, 1]
Nuevo contenido de la lista: [111, 5, 7, 2, 1]

Copiar valores entre índices

También podemos tomar el valor guardado en una posición de la lista y copiarlo dentro de otra posición. Imagina que queremos que el segundo elemento (índice 1) tome una copia exacta del valor que tiene actualmente el quinto elemento (índice 4):

numbers = [10, 5, 7, 2, 1]

numbers[0] = 111          # El índice 0 ahora vale 111
numbers[1] = numbers[4]   # El índice 1 copia el valor que hay en el índice 4 (que es un 1)

print("Contenido final de la lista:", numbers)
Contenido final de la lista: [111, 1, 7, 2, 1]

Expresiones como Índices (Pista para el Examen)

En los ejemplos anteriores hemos usado números literales fijos (0, 1, 4) dentro de los corchetes. Sin embargo, Python permite que cualquier expresión matemática que devuelva un número entero actúe como índice. Esto incluye variables o sumas. Mira este escenario clásico que podría aparecer en una pregunta del PCEP:

i = 2
numbers = [10, 5, 7, 2, 1]

# Python evalúa la operación matemática (2 + 1 = 3) y apunta al índice 3
numbers[i + 1] = 99 

print(numbers)
[10, 5, 7, 99, 1]

Resumen

  • La operación de seleccionar un elemento se conoce como indexación.
  • Las listas son mutables: puedes cambiar sus elementos individuales usando lista[indice] = nuevo_valor.
  • Al copiar un elemento sobre otro (lista[A] = lista[B]), el valor antiguo en la posición A se pierde permanentemente.
  • El valor dentro de los corchetes de indexación puede ser una variable o una operación, siempre y cuando su resultado final sea un número entero (int).

Aquí tienes la adaptación de esta sección, donde el curso profundiza en el acceso individual y global a los elementos de una lista, e introduce una de las funciones integradas más utilizadas de la certificación PCEP: len().

Acceso al Contenido y la Función Dinámica len()

Una de las grandes ventajas de Python es la flexibilidad con la que te permite visualizar la información de las colecciones de datos, ya sea inspeccionando un elemento específico o auditando la estructura completa.

1. Acceso Individual (Por Escalar)

Si necesitas extraer o mostrar un único dato aislado de la lista, utilizas su índice dentro de la función print():

print(numbers[0])  # Accede al primer elemento de la lista

Suponiendo que se hayan completado con éxito las reasignaciones de la sección anterior, esta instrucción enviará el entero 111 a la consola de forma limpia, tratándolo como una variable escalar común.

2. Acceso Estructural (Impresión Global)

A diferencia de otros lenguajes de programación de bajo nivel que requieren un bucle para mostrar un arreglo, Python te permite imprimir una lista completa pasando únicamente su nombre como argumento:

print(numbers)  # Imprime la lista completa de golpe

Al hacerlo, Python adorna automáticamente la salida en la consola agregando los corchetes cuadrados [] y separando los elementos con comas, indicándole explícitamente al desarrollador que lo que está viendo es una estructura de lista:

[111, 1, 7, 2, 1]

La Función len() (Crucial para Bucles)

Las listas en Python son entidades altamente dinámicas. Esto significa que su tamaño no es fijo; la longitud de una lista puede crecer o encogerse drásticamente durante la ejecución del script a medida que agregas nuevos elementos o eliminas otros.

Para averiguar el número exacto de elementos que contiene una lista en un momento determinado, utilizamos la función integrada len() (cuyo nombre proviene de length, longitud).

numbers = [10, 5, 7, 2, 1]
print("Longitud de la lista:", len(numbers))

Comportamiento e Interpretación:

  • La función len() recibe el nombre de la lista como argumento y devuelve un número entero (int) que representa la cantidad total de datos almacenados en ella.
  • Para el código del editor provisto en el sandbox, la consola imprimirá exactamente: Longitud de la lista: 5.
  • Regla de oro para el PCEP: Recuerda siempre la diferencia entre longitud e índices. Si len(numbers) devuelve 5, el último índice válido disponible en esa lista será siempre la longitud menos uno: 4.

Eliminación de Elementos: La Instrucción del

Dado que las listas en Python son completamente dinámicas, no solo puedes cambiar el valor de sus elementos o medir su tamaño, sino que también puedes hacer desaparecer elementos por completo en cualquier momento del programa.

Para lograr esto, Python implementa una palabra clave especial llamada del (del inglés delete, borrar).

⚠️ Nota Teórica de Examen: del es una instrucción (una palabra clave reservada del lenguaje), no una función. Por lo tanto, no lleva paréntesis; simplemente se escribe seguida del elemento indexado que deseas destruir.

Cuando eliminas un elemento, este se desvanece de la memoria, la longitud de la lista se reduce automáticamente en uno y, lo más importante, los elementos que estaban a la derecha se desplazan una posición hacia la izquierda para llenar el vacío, reconfigurando sus índices. Analicemos el comportamiento del código del curso:

numbers = [111, 1, 7, 2, 1]  # Estado inicial de la lista (Longitud: 5)

del numbers[1]               # Eliminamos el elemento en el índice 1 (el número 1)

print(len(numbers))          # Imprime la nueva longitud
print(numbers)               # Imprime el contenido actual
4
[111, 7, 2, 1]

Análisis del movimiento de índices:

  • El elemento 111 se queda intacto en el índice 0.
  • El número 1 que ocupaba el índice 1 desapareció para siempre.
  • El número 7, que antes estaba en el índice 2, ahora pasa a ocupar el índice 1.
  • El número 2 pasa del índice 3 al índice 2.
  • El número 1 del final pasa del índice 4 al índice 3.

El Peligro del IndexError (Vital para el Test)

Un error clásico que el Python Institute coloca en los cuestionarios de certificación consiste en intentar acceder a una posición que existía hace unas líneas, pero que ha quedado fuera de rango tras una eliminación. Si inmediatamente después del código anterior intentas hacer esto:

print(numbers[4])
# o también:
numbers[4] = 1

El programa colapsará inmediatamente y detendrá su ejecución, arrojando un error en tiempo de ejecución de tipo IndexError: list assignment index out of range (o list index out of range).

¿Por qué ocurre? Como la lista ahora solo tiene 4 elementos, sus índices válidos se han reestructurado y van estrictamente del 0 al 3. El índice 4 ya no existe en la memoria de la computadora. No puedes leer datos de una posición inexistente, y mucho menos reasignarle un valor.

El Sistema de Indexación Inversa

La regla matemática y posicional que utiliza Python para la indexación negativa es la siguiente:

  • El índice -1: Representa siempre el último elemento de la lista.
  • El índice -2: Representa el penúltimo elemento (el elemento justo antes del último).
  • El índice -3: Representa el elemento antepenúltimo, y así sucesivamente hacia atrás.

Volvamos a tomar el estado de nuestra lista tras la eliminación del artículo anterior:

numbers = [111, 7, 2, 1]

Si aplicamos indexación inversa en este script, podemos acceder a los datos de forma directa:

print(numbers[-1])  # Imprime el último elemento: 1
print(numbers[-2])  # Imprime el penúltimo elemento: 2
print(numbers[-3])  # Imprime el elemento anterior: 7
print(numbers[-4])  # Imprime el primer elemento de la lista: 111

El Límite de la Inversión (Evitando el IndexError)

Así como en la indexación positiva no puedes pasarte del límite derecho (N – 1), en la indexación negativa no puedes pasarte del límite izquierdo. Dado que nuestra lista actual consta exactamente de 4 elementos, el primer elemento (111) se alcanza en el índice -4. Eso significa que -4 es el límite absoluto de nuestro viaje hacia atrás.

¿Por qué es tan útil en el mundo real?

Imagínate que estás procesando un flujo constante de datos en tiempo real (como los precios de las acciones de una empresa en la bolsa o lecturas de un sensor de temperatura) y la lista no para de crecer. Si quisieras acceder al último dato registrado usando índices positivos, tendrías que escribir una sintaxis un poco engorrosa combinando la función len():

# Enfoque positivo manual:
ultimo_elemento = datos[len(datos) - 1]

Con la indexación negativa, no te importa si la lista tiene 4, 500 o 10 millones de elementos; puedes acceder al último registro de forma instantánea y limpia:

# Enfoque nativo de Python:
ultimo_elemento = datos[-1]

Aquí tienes la adaptación de esta sección práctica fundamental. El Python Institute utiliza este artículo para enseñarte las dos formas estándar de expandir una lista en Python: añadir al final con .append() o inyectar en cualquier posición con .insert().

Estas dos herramientas cambian por completo la longitud de las colecciones y son un foco constante de ejercicios de rastreo en el examen PCEP.

Los Métodos .append() e .insert()

Como se explica en el articulo anterior los métodos pertenecen a los datos, para hacer crecer una lista debemos usar la sintaxis de punto (lista.metodo()). Python nos ofrece dos estrategias distintas para inyectar nuevos elementos en un contenedor existente.

1. El Método .append() (Añadir al Final)

Si lo único que quieres es “pegar” un nuevo dato justo al final de la lista actual, el método idóneo es .append() (del inglés adjuntar o anexar).

lista.append(valor)

Comportamiento:

  • Toma el valor que le pases como único argumento y lo coloca en una posición nueva al final de la lista.
  • La longitud de la lista aumenta automáticamente en uno ($len + 1$).
  • No altera las posiciones ni los índices de ninguno de los elementos que ya estaban guardados.

2. El Método .insert() (Insertar en cualquier posición)

El método .insert() es más sofisticado y potente: te permite introducir un nuevo elemento en cualquier coordenada de la lista, no solo al final.

lista.insert(posicion, valor)

Comportamiento:

Recibe estrictamente dos argumentos:

  1. posicion (Índice): El número de índice exacto donde quieres que se aloje el nuevo dato.
  2. valor: El elemento o dato que vas a inyectar.

⚠️ La reestructuración de índices hacia la derecha: Para abrir espacio al nuevo elemento, Python realiza un movimiento masivo: todos los elementos que ya existían y ocupaban posiciones desde ese índice hacia la derecha se desplazan una posición hacia la derecha.

Rastreo del Experimento

Vamos a realizar el seguimiento del código combinando ambos métodos paso a paso:

numbers = [111, 7, 2, 1]  # Estado inicial de la lista

# Experimento 1: Añadimos al final
numbers.append(4)
print(numbers)            # Salida: [111, 7, 2, 1, 4]

# Experimento 2: Insertamos usando la línea del enunciado
numbers.insert(1, 333)
print(numbers)
[111, 333, 7, 2, 1, 4]

Análisis del movimiento de memoria:

  • El 111 se queda exactamente donde estaba (índice 0).
  • El número 333 se adueña con éxito del índice 1.
  • El 7, que antes era el segundo elemento (índice 1), ahora es desplazado y se convierte en el tercero (índice 2).
  • El 2 pasa del índice 2 al 3.
  • El 1 pasa del índice 3 al 4.
  • El 4 (que metimos con el append) pasa del índice 4 al 5.

Resumen

  • .append(valor) añade un elemento al final de la lista; solo requiere un argumento.
  • .insert(indice, valor) añade un elemento en una posición específica; requiere dos argumentos.
  • Usar .insert() desplaza los elementos preexistentes hacia la derecha, incrementando sus índices en 1.
  • Ambos métodos modifican la lista original de forma directa (in-place) y aumentan su longitud (len()) en 1.

Aquí tienes la adaptación de esta sección, donde el curso explora un patrón de diseño fundamental en la programación: la creación de listas dinámicas desde cero.

El Python Institute utiliza estos dos ejemplos interactivos para evaluar tu agudeza visual y tu capacidad de rastrear cómo la elección entre .append() e .insert() cambia por completo el orden de los datos finales.

El Ciclo de Vida de una Lista: Inicialización Vacía

En el desarrollo de software real, rara vez conocemos de antemano todos los datos que van a componer nuestras colecciones. Lo habitual es crear una lista completamente vacía y permitir que el programa la vaya alimentando de forma dinámica a medida que procesa información (lecturas de sensores, entradas de usuario o iteraciones matemáticas).

my_list = []  # Un contenedor con longitud (len) igual a 0

A partir de esta base limpia, analicemos los dos experimentos de construcción dinámicos que plantea el curso.

Experimento 1: Construcción Secuencial con .append()

Imagina que ejecutamos este bucle básico (la primera variante descrita por el texto):

my_list = []

for i in range(5):
    my_list.append(i + 1)

print(my_list)

Rastreo de memoria paso a paso:

  • El range(5) produce los números del 0 al 4. En cada vuelta, sumamos i + 1.
  • Vuelta 1 (i=0): i + 1 = 1. Se añade al final. $\rightarrow$ [1]
  • Vuelta 2 (i=1): i + 1 = 2. Se añade al final. $\rightarrow$ [1, 2]
  • Vuelta 3 (i=2): i + 1 = 3. Se añade al final. $\rightarrow$ [1, 2, 3]
  • Vuelta 4 (i=3): i + 1 = 4. Se añade al final. $\rightarrow$ [1, 2, 3, 4]
  • Vuelta 5 (i=4): i + 1 = 5. Se añade al final. $\rightarrow$ [1, 2, 3, 4, 5]
  • Salida en consola: [1, 2, 3, 4, 5] (Una secuencia natural y ascendente).

Experimento 2: Construcción Inversa con .insert()

Ahora analicemos la modificación exacta que introduce el artículo en su Sandbox, donde cambiamos el método de inserción:

my_list = []

for i in range(5):
    my_list.insert(0, i + 1)

print(my_list)

Rastreo de memoria paso a paso:

Fíjate bien: el primer argumento de .insert() es un 0. Esto significa que estamos obligando a cada nuevo número a inyectarse siempre en la primerísima posición, empujando a todos los números anteriores hacia la derecha.

  • Vuelta 1 (i=0): Se inserta 1 en la posición 0. — [1]
  • Vuelta 2 (i=1): Se inserta 2 en la posición 0. El 1 es empujado a la derecha. — [2, 1]
  • Vuelta 3 (i=2): Se inserta 3 en la posición 0. Los demás se desplazan. — [3, 2, 1]
  • Vuelta 4 (i=3): Se inserta 4 en la posición 0. — [4, 3, 2, 1]
  • Vuelta 5 (i=4): Se inserta 5 en la posición 0. — [5, 4, 3, 2, 1]
  • Salida exacta en consola:
[5, 4, 3, 2, 1]

Ambos bloques de código procesaron exactamente los mismos números de entrada, pero la mecánica interna de los métodos alteró el resultado drásticamente. Mientras que .append() genera una fila ordenada por orden de llegada, .insert(0, valor) actúa como una pila de platos: el último en llegar siempre se coloca encima de todos los demás.

Procesamiento de Colecciones: El Bucle for y las Listas

Una de las tareas más comunes al trabajar con estructuras de datos es recorrerlas por completo para analizar, transformar o acumular sus valores (por ejemplo, calcular el promedio de notas de un grupo, buscar un elemento o sumar montos). El curso nos propone el reto clásico de sumar todos los números de una lista, mostrándonos las dos “caras” que tiene el bucle for para resolverlo.

🚨 Buena Práctica de Examen: Para guardar el acumulado de la suma, el curso crea una variable llamada total. Nunca nombres a tu variable sum. En Python, sum() es una función integrada de fábrica. Si creas una variable con ese mismo nombre, destruirás el acceso a la función original, lo cual se considera un error grave de diseño.

Enfoque 1: El Método Tradicional por Indexación

La primera forma de resolver el problema es la que hereda la lógica de lenguajes como C o Java. Consiste en generar una secuencia de números que sirvan como “punteros” o índices para ir extrayendo los datos uno a uno:

my_list = [10, 1, 8, 3, 5]
total = 0

# range(5) genera los índices: 0, 1, 2, 3, 4
for i in range(len(my_list)):
    total += my_list[i]  # Accedemos manualmente usando el índice 'i'

print(total)  # Salida: 27

Análisis del flujo:

  • Usamos la función len(my_list) dentro del range(). Esto es excelente porque hace que el código sea dinámico y heredable: si la lista cambia de tamaño en el futuro, el bucle se adaptará automáticamente sin romperse.
  • En cada vuelta, la variable i toma un número de posición (0, luego 1, luego 2…) y extrae el escalar guardado en my_list[i] para sumarlo al total.

Enfoque 2: La “Segunda Cara” del Bucle for (Recorrido Directo)

Python fue diseñado para ser limpio y legible. Por eso, ofrece una variante del bucle for capaz de ocultar toda la matemática de los índices tras bambalinas, entregándote los elementos directamente en la mano. Observa esta belleza de optimización:

my_list = [10, 1, 8, 3, 5]
total = 0

# Recorrido directo sobre el contenedor
for i in my_list:
    total += i

print(total)  # Salida: 27

¿Qué está pasando aquí? (Clave para el Test)

  • Adiós a los índices: Ya no necesitas usar range() ni len().
  • Lectura natural: La instrucción se lee literalmente como: “Para cada elemento i dentro de my_list.
  • Copia en memoria: En la primera vuelta, Python va a la lista, toma el primer elemento (10) y se lo asigna directamente a i. En la segunda vuelta, i se convierte en 1, luego en 8, luego en 3 y finalmente en 5.
  • La variable i ya no es un número de posición; es una copia exacta del valor del elemento actual. El bucle sabe exactamente cuándo detenerse porque se ejecuta tantas veces como elementos existan en el contenedor.

El Arte del Intercambio (Swapping) en Python

Imagina que necesitas reorganizar por completo los elementos de nuestra lista para invertir su orden. Es decir, queremos que el primer elemento se intercambie con el quinto, y el segundo con el cuarto (el del medio se quedaría intacto).

Python fue diseñado bajo la filosofía de que el código debe ser lo más limpio y legible posible. Por ello, implementa una característica nativa y elegante que permite realizar el intercambio en una sola línea de código y sin usar variables auxiliares:

variable_1 = 1
variable_2 = 2

# ¡El gran truco de Python!
variable_1, variable_2 = variable_2, variable_1

¿Cómo funciona esto por dentro? (Clave para el Test)

Este fenómeno se basa en un concepto técnico llamado empaquetamiento y desempaquetamiento de tuplas (que estudiarás a fondo al final del Módulo 3). Cuando Python ve la línea variable_1, variable_2 = variable_2, variable_1, realiza la evaluación de la siguiente manera:

  1. Paso Izquierdo (Evaluación): Primero mira el lado derecho del signo igual e identifica los valores actuales de las variables en la memoria de la computadora: crea una estructura temporal con los valores (2, 1).
  2. Paso Derecho (Asignación): De forma simultánea y en un solo microsegundo de hardware, desempaqueta esos valores y los asigna a las variables del lado izquierdo en el orden correspondiente.

Resumen

  • El intercambio tradicional requiere una tercera variable auxiliar para no destruir los datos en memoria.
  • La sintaxis A, B = B, A es la forma nativa y recomendada en Python (Pythonic) para intercambiar valores.
  • Esta operación evalúa todo el lado derecho antes de modificar las variables del lado izquierdo, garantizando que ningún valor se pierda en el proceso.

Inversión del orden:.reverse()

Todas las listas en Python vienen equipadas de fábrica con el método integrado .reverse().

lst = [5, 3, 1, 2, 4]
print("Original:", lst)

lst.reverse()
print("Invertida:", lst)  # Salida: [4, 2, 1, 3, 5]

Características Clave para la Certificación PCEP

  • Mutación In-Place (Directa): Al igual que .sort(), .reverse() es un método, por lo que se invoca mediante la sintaxis de punto. No crea una lista nueva ni devuelve un resultado; modifica directamente la estructura de la lista original en la memoria.
  • Inversión No Es Ordenamiento: Un error muy común en los exámenes es confundir “invertir” con “ordenar de forma descendente”. El método .reverse() no analiza si un número es mayor o menor que otro; simplemente toma el orden actual de la lista y lo voltea como un espejo (el primero pasa a ser el último, el segundo pasa a ser el penúltimo, etc.).

El Método .sort() (Ordenamiento)

Si necesitas que Python ordene cualquier lista de forma automática, eficiente y en una sola línea de código, utilizas el método nativo .sort():

my_list = [8, 10, 6, 2, 4]
my_list.sort()

print(my_list)  # Salida en consola: [2, 4, 6, 8, 10]

Características Clave para el Examen PCEP:

  • Es un método, no una función: Se invoca utilizando la sintaxis de punto (lista.sort()). No genera una lista nueva, sino que modifica la lista original de forma directa en la memoria (operación in-place).
  • Alta eficiencia: Tras bambalinas, .sort() no utiliza el método de la burbuja. Implementa un algoritmo altamente optimizado llamado Timsort (una combinación híbrida de ordenamiento por inserción y ordenamiento por mezcla), diseñado para ser sumamente rápido en cualquier escenario.
  • Orden predeterminado: Si no se le pasan argumentos adicionales, ordena los elementos de forma estrictamente ascendente (de menor a mayor).

La Vida Interna de las Listas: Punteros y Gestión de Memoria

Hasta ahora, hemos tratado a las listas como si fueran variables normales, pero las colecciones (y otras estructuras complejas en Python) se comportan bajo reglas de juego completamente distintas a las de las variables escalares. Analicemos el inquietante experimento que plantea el curso:

list_1 = [1]
list_2 = list_1
list_1[0] = 2

print(list_2)
[2]

Si aplicáramos la lógica de las variables escalares comunes, pensaríamos que list_2 mantiene su valor original y la consola debería imprimir [1]. Sin embargo, al ejecutar este programa, la consola muestra de forma sorprendente:

¿Por qué modificar list_1 alteró mágicamente el contenido de list_2?

Variables Escalares vs. Referencias de Memoria

Para entender este fenómeno, debemos repasar cómo se guardan los datos en la memoria de la computadora:

1. Variables Escalares (Datos por Valor)

El nombre de una variable ordinaria (como un entero o un flotante) es el dueño directo de su contenido. Si escribes a = 10 y luego b = a, Python crea una copia física independiente del valor 10 en una nueva celda de memoria para b. Si modificas a, b no se entera.

Listas (Datos por Referencia o Puntero)

El nombre de una lista NO almacena los datos de la lista; almacena la dirección de memoria (la ubicación) donde está guardada esa lista. Cuando ejecutas la instrucción de asignación:

list_2 = list_1

Python no está creando una lista nueva ni está copiando los elementos internos. Lo único que hace es copiar el “letrero” o la dirección de memoria.

En efecto, ahora tienes dos nombres diferentes (list_1 y list_2) que están apuntando exactamente a la misma e idéntica ubicación en la memoria del ordenador. Son dos llaves para abrir la misma caja. Si usas la llave de list_1 para cambiar el contenido del contenedor, al abrir la caja con la llave de list_2 verás, inevitablemente, ese mismo cambio.

¿Cómo solucionamos esto?

¿Cómo rompemos este vínculo para obligar a Python a duplicar físicamente la lista en otra dirección de memoria? La respuesta corta es: utilizando el rebanado (slicing), una sintaxis especial que le ordena a Python clonar el contenido en lugar de la dirección.

El Rebanado (Slicing): La Solución Definitiva

En la sección anterior descubrimos el gran peligro de la gestión de memoria en Python: usar el signo = entre listas solo copia la dirección de memoria, vinculándolas permanentemente.

Afortunadamente, la solución nativa está al alcance de tus dedos: el rebanado o slice. Esta característica sintáctica le ordena explícitamente a Python que extraiga y copie el contenido físico de los elementos, generando una lista completamente nueva e independiente en la memoria. Observa el primer experimento corregido:

list_1 = [1]
list_2 = list_1[:]  # ¡El truco de la rebanada completa!
list_1[0] = 2

print(list_2)  # Salida en consola: [1]

Gracias a ese sutil operador de dos puntos [:], list_2 clonó el valor interno y se convirtió en un contenedor independiente. Modificar list_1 ya no afecta en absoluto a list_2.

La Sintaxis General del Rebanado

El rebanado no solo sirve para copiar listas enteras; su propósito principal es extraer fragmentos o “sublistas” específicas. Su estructura básica se escribe así:

mi_lista[start : end]

A primera vista se parece a la indexación estándar, pero la presencia de los dos puntos (:) dentro de los corchetes lo cambia todo. Esta instrucción toma elementos de la lista de origen partiendo desde el índice start hasta llegar al índice end - 1.

⚠️ Regla de Oro Inquebrantable del PCEP: El límite derecho (end) es excluyente (no se incluye en el resultado). El elemento que se encuentra en la posición end es el primer elemento que no forma parte de la rebanada.

La Matemática del Rebanado (Rastreo del Test)

Analicemos el segundo ejemplo que plantea el curso:

my_list = [10, 8, 6, 4, 2]
new_list = my_list[1:3]

print(new_list)  # Salida en consola: [8, 6]

¿Cómo calcula Python el resultado?

  1. Punto de partida (start = 1): Va al índice 1 de la lista original, donde se encuentra el número 8. Este elemento sí entra.
  2. Punto de parada (end = 3): El índice 3 contiene al número 4. Como el límite superior es excluyente, el viaje se detiene justo antes. El último índice extraído es el 2 (donde está el 6).
  3. Tamaño resultante: Existe una fórmula matemática infalible para saber cuántos elementos tendrá tu nueva lista sin necesidad de contar a mano: $\text{longitud} = \text{end} – \text{start}$. En este caso: $3 – 1 = 2$ elementos.

Resumen

  • El operador [:] clona el contenido de una lista en una nueva dirección de memoria, rompiendo el vínculo de referencia.
  • En la sintaxis [start:end], el índice start se incluye, pero el índice end se excluye.
  • La sublista resultante abarcará estrictamente los elementos desde el índice start hasta el índice end - 1.
  • El número total de elementos extraídos siempre responderá a la operación matemático-entera: $end – start$.
  • Al igual que en la indexación normal, es totalmente legal utilizar índices negativos para el start y el end para contar desde el final de la lista hacia atrás.

Aquí tienes la adaptación de esta sección, donde el curso eleva la complejidad combinando el rebanado (slicing) con los índices negativos.

El Python Institute diseña una gran cantidad de preguntas del examen PCEP mezclando límites positivos y negativos en una misma rebanada para comprobar si eres capaz de calcular mentalmente el rango resultante o identificar cuándo una rebanada da como resultado una lista vacía.

Rebanados Avanzados: El Uso de Índices Negativos

El rebanado mantiene sus dos reglas de oro inquebrantables sin importar si usas números positivos o negativos:

  1. El parámetro start indica la posición del primer elemento incluido.
  2. El parámetro end indica la posición del primer elemento excluido (se extrae hasta end - 1).

Cuando introduces índices negativos en un slice, la lógica de Python simplemente traduce esas posiciones contando desde el final de la lista hacia atrás (donde -1 es el último elemento).

Caso 1: Mezcla de Índices Positivos y Negativos

Analicemos el primer ejemplo del artículo:

my_list = [10, 8, 6, 4, 2]
new_list = my_list[1:-1]

print(new_list)  # Salida en consola: [8, 6, 4]

Rastreo e Interpretación en Memoria:

  • start = 1: Apunta al índice positivo 1, donde se encuentra el número 8 (se incluye).
  • end = -1: Apunta al último elemento de la lista, que es el número 2. Como el límite superior es excluyente, este elemento se queda fuera de la rebanada. El viaje se detiene justo antes.
  • Resultado: Python extrae de forma secuencial todo lo que se encuentra entre el índice 1 y el penúltimo elemento. Por lo tanto, toma el 8, el 6 y el 4.

Caso 2: El Choque de Límites (Rebanadas Vacías)

¿Qué ocurre si, por error o lógica del programa, el punto de partida (start) se encuentra en una posición que está más a la derecha que el punto de parada (end)? Mira el segundo escenario propuesto:

my_list = [10, 8, 6, 4, 2]
new_list = my_list[-1:1]

print(new_list)  # Salida en consola: []

¿Por qué devuelve una lista vacía []?

  • start = -1: Le ordena a Python empezar en el último elemento (el número 2, en el extremo derecho).
  • end = 1: Le ordena detenerse en el índice 1 (el número 8, en el extremo izquierdo).
  • El comportamiento nativo: Por defecto, el rebanado estándar de Python siempre viaja de izquierda a derecha (en sentido incremental). Python no puede retroceder de forma automática desde el final hacia el principio de la lista bajo esta sintaxis básica.

Al intentar iniciar en un punto avanzado y buscar un límite que quedó atrás, Python no arroja ningún error de ejecución (IndexError); simplemente devuelve una lista completamente vacía [].

Atajos de Rebanado: Parámetros Omitidos

El rebanado en Python es tan flexible que no te obliga a rellenar siempre los dos campos (start y end). Si dejas uno de los lados de los dos puntos (:) completamente vacío, Python activará un comportamiento predeterminado automático.

1. Omitir el inicio ([:end])

Si omites el parámetro start a la izquierda de los dos puntos, Python asume de forma automática que quieres iniciar la rebanada desde el primerísimo elemento de la lista, es decir, desde el índice 0. Por lo tanto, escribir esto:

mi_lista[:end]

Es el equivalente exacto (pero más compacto y elegante) de escribir mi_lista[0:end]. Analicemos el primer ejemplo del artículo:

my_list = [10, 8, 6, 4, 2]
new_list = my_list[:3]

print(new_list)  # Salida en consola: [10, 8, 6]

Rastreo: Python traduce la instrucción como my_list[0:3]. Comienza en el índice 0 (el número 10) y extrae secuencialmente los elementos hasta llegar al índice 2 (el número 6), dejando fuera el índice 3.

2. Omitir el final ([start:])

De forma simétrica, si dejas vacío el espacio a la derecha de los dos puntos (end), Python asumirá que deseas que la rebanada se extienda hasta el final absoluto de la lista, incluyendo el último elemento. Técnicamente, escribir esto:

mi_lista[start:]

Es el equivalente exacto de escribir mi_lista[start:len(mi_lista)]. Analizemos el segundo ejemplo del artículo:

my_list = [10, 8, 6, 4, 2]
new_list = my_list[3:]

print(new_list)  # Salida en consola: [4, 2]

Rastreo: El corte inicia estrictamente en el índice 3 (donde habita el número 4). Al no haber un límite de parada, el rebanado continúa avanzando y absorbe también al número 2 (índice 4), completando el viaje hasta el límite total determinado por la longitud de la lista.

Resumen

  • lista[:3] extrae los elementos desde el principio (índice 0) hasta el índice 2.
  • lista[3:] extrae los elementos desde el índice 3 hasta el último elemento disponible de la lista.
  • lista[:] combina ambos atajos: omite el inicio y el final, lo que le indica a Python que extraiga todo el contenido desde el índice 0 hasta el final, generando una copia física idéntica e independiente de la lista completa.

Aquí tienes la adaptación de esta sección clave. El Python Institute utiliza este artículo para enseñarte cómo interactúan el rebanado (slicing) y la instrucción del. Esta combinación es sumamente peligrosa en el examen PCEP porque cambia drásticamente de significado con solo quitar o poner dos puntos (:).

Operaciones Avanzadas: Borrado Masivo con Rebanados

Como ya establecimos, omitir tanto el inicio como el final usando el operador [:] le ordena a Python extraer todo el contenido desde el principio hasta el fin, generando una copia física idéntica e independiente de la lista completa en una nueva dirección de memoria:

my_list = [10, 8, 6, 4, 2]
new_list = my_list[:]  # Duplicación física en memoria

print(new_list)        # Salida: [10, 8, 6, 4, 2]

Ahora bien, la instrucción del (que ya usamos para borrar un único elemento como del my_list[0]) se vuelve mucho más potente cuando la aplicamos sobre un rebanado. Nos permite realizar modificaciones estructurales masivas de tres formas distintas:

1. Eliminar una sublista específica (del mi_lista[start:end])

Puedes usar del para extirpar un bloque entero de elementos de una sola vez. Al hacerlo, se aplican las mismas reglas de los índices espejos y excluyentes:

my_list = [10, 8, 6, 4, 2]
del my_list[1:3]

print(my_list)  # Salida en consola: [10, 4, 2]

Comportamiento interno (Clave de examen):

  • El rebanado [1:3] apunta a los índices 1 y 2 (los números 8 y 6). El índice 3 queda excluido del borrado.
  • Python elimina esos dos elementos directamente sobre la lista original. En este caso, el rebanado NO genera una lista nueva; actúa como un selector de destrucción.
  • Los elementos que quedan a la derecha (4 y 2) se desplazan automáticamente hacia la izquierda para rellenar el vacío, reestructurando los índices de la lista.

2. Vaciar la lista por completo (del mi_lista[:])

Si aplicas la instrucción del sobre la rebanada completa [:], le estás ordenando a Python que elimine absolutamente todos los elementos que habitan dentro del contenedor, desde el índice 0 hasta el último.

my_list = [10, 8, 6, 4, 2]
del my_list[:]

print(my_list)  # Salida en consola: []
  • Resultado: La estructura de la lista sobrevive, pero se queda totalmente desierta. Te devuelve una lista vacía [] cuya longitud (len()) pasa a ser exactamente 0. El letrero u objeto de la variable sigue existiendo en el programa.

3. Destruir la variable por completo (del mi_lista)

Quitar los dos puntos ([:]) de la ecuación cambia por completo el destino de tu programa. Si ejecutas del directamente sobre el nombre de la lista, ya no estás interactuando con su contenido:

my_list = [10, 8, 6, 4, 2]
del my_list

# print(my_list)  # <-- ¡ERROR DE EJECUCIÓN!

¿Qué pasa aquí?

La instrucción destruye el “letrero” o el identificador my_list de la memoria del sistema de forma fulminante. La variable deja de existir. Si intentas usar o imprimir my_list en la línea siguiente, el intérprete de Python colapsará lanzando un NameError: name 'my_list' is not defined.

Resumen

  • del lista[1:3] elimina un fragmento de elementos de forma directa (in-place) y reajusta los índices restantes.
  • del lista[:] borra todo el contenido interno pero mantiene viva la lista vacía [] en memoria.
  • del lista destruye por completo el identificador de la variable, provocando un error de tipo NameError si se intenta invocar después.

Aquí tienes la adaptación de esta sección, donde el curso presenta dos de los operadores más intuitivos, potentes y utilizados en Python: in y not in.

El Python Institute recurre constantemente a estos operadores en el examen PCEP, integrándolos dentro de estructuras condicionales (if-else) para evaluar tu capacidad de determinar si un elemento específico habita o no dentro de una colección, ahorrándonos la necesidad de escribir bucles manuales de búsqueda.

Operadores de Membresía: in y not in

Cuando trabajas con colecciones de datos, una de las preguntas más recurrentes que tu programa debe responder es: ¿Está este dato guardado dentro de la lista? En lenguajes tradicionales, resolver esto requiere escribir un bucle for que examine uno a uno los elementos y una variable bandera para guardar el resultado. Python, buscando la máxima legibilidad, resuelve este problema de raíz ofreciendo dos operadores de pertenencia o membresía que realizan toda la búsqueda tras bambalinas:

elemento in mi_lista
elemento not in mi_lista

Ambos operadores pertenecen a la categoría de operadores lógicos o de comparación, lo que significa que su ejecución siempre va a devolver estrictamente un valor booleano: True o False.

1. El Operador in (¿Está dentro?)

El operador in analiza la lista (su argumento derecho) buscando el valor que le indiques a su izquierda.

  • Devuelve True si el elemento se encuentra guardado en cualquier posición de la lista.
  • Devuelve False si recorre toda la lista y no encuentra ninguna coincidencia.
my_list = [0, 3, 12, 8, 2]

print(5 in my_list)   # Salida: False (El 5 no está en la lista)
print(12 in my_list)  # Salida: True  (El 12 está en el índice 2)

2. El Operador not in (¿No está dentro?)

El operador not in funciona de forma simétrica pero inversa: evalúa la ausencia del dato.

  • Devuelve True si el elemento NO está en la lista (confirma que está ausente).
  • Devuelve False si el elemento sí está presente dentro de la lista.
my_list = [0, 3, 12, 8, 2]

print(5 not in my_list)   # Salida: True  (Es verdad, el 5 NO está dentro)
print(12 not in my_list)  # Salida: False (Es falso, porque el 12 SÍ está dentro)

El Experimento del Curso en Acción

Combinando estos operadores dentro de condicionales if, podemos ramificar las decisiones del programa de forma limpia y directa:

my_list = [0, 3, 12, 8, 2]
value = int(input("Introduce el número a buscar: "))

if value in my_list:
    print("¡Bingo! El elemento forma parte de la lista.")
else:
    print("Lo siento, ese valor no se encuentra aquí.")

Resumen

  • in y not in son operadores de membresía y su resultado es siempre un booleano (True / False).
  • El argumento de la izquierda es el valor que se busca; el argumento de la derecha debe ser una colección (como una lista).
  • Estos operadores realizan un escaneo secuencial interno por toda la estructura de datos de manera automática.

Listas dentro de Listas y Listas de Comprensión

Hasta ahora, hemos trabajado con listas que contienen elementos individuales o escalares (números, cadenas, booleanos). Sin embargo, en el desarrollo real es extremadamente común encontrarnos con estructuras de datos más complejas, como listas cuyos elementos son otras listas.

El ejemplo clásico de la vida real para entender esta estructura bidimensional es un tablero de ajedrez. Un tablero se compone de un plano con 8 filas y 8 columnas. Si quisiéramos representar este tablero de forma digital, podríamos asumir que cada una de las 8 filas es una lista individual que contiene 8 elementos (las casillas o piezas).

Construcción Tradicional frente a Listas de Comprensión

Ejemplo 1: El Generador de Cuadrados

squares = [x ** 2 for x in range(10)]
print(squares)
[0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
  • Mecánica: El range(10) genera números del 0 al 9. En cada iteración, el número actual x se eleva al cuadrado (x 2) antes de inyectarse en el contenedor.

Ejemplo 2: Las Potencias de Dos

twos = [2 ** i for i in range(8)]
print(twos)
[1, 2, 4, 8, 16, 32, 64, 128] 
  • Mecánica: El bucle itera 8 veces (i va de 0 a 7). En cada ciclo, calcula la potencia matemática 2**i

Ejemplo 3: Listas de Comprensión con Condicionales (if)

Las listas de comprensión pueden incluir filtros lógicos al final de la instrucción para decidir si un dato merece o no ser incluido en la colección final:

# Filtramos la lista 'squares' del Ejemplo 1
squares = [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
odds = [x for x in squares if x % 2 != 0]
print(odds)
[1, 9, 25, 49, 81]
  • Mecánica: Recorremos directamente la lista squares. Para cada número x, evaluamos el condicional if x % 2 != 0 (operador residuo para detectar si un número es impar). Si la condición se cumple (True), el elemento x se guarda; si es par, se descarta.

Resumen

  • Una lista de comprensión es una forma compacta y elegante de crear una lista dinámicamente en una sola línea.
  • Sintaxis básica: [expresion for variable in iterable].
  • Sintaxis con filtro: [expresion for variable in iterable if condicion].
  • El filtro if evalúa cada elemento y solo permite la inserción de aquellos que devuelvan True.