Python & AI Tutorials Logo
Programación Python

18. Modelo de datos y objetos de Python: referencias, comparaciones y copias

Comprender cómo Python almacena y gestiona los datos es crucial para escribir programas correctos. En este capítulo, exploraremos el modelo de objetos(object model) de Python—el sistema fundamental que gobierna cómo funcionan todos los datos en Python. Aprenderás por qué algunas asignaciones crean copias independientes mientras que otras crean referencias compartidas, cómo comparar objetos correctamente y cómo evitar errores comunes al trabajar con colecciones.

Este conocimiento te ayudará a entender comportamientos sorprendentes que quizá hayas encontrado, como por qué modificar una lista(list) a veces afecta a otra, o por qué comparar dos listas con == da resultados diferentes que compararlas con is.

18.1) Todo es un objeto en Python

En Python, cada pieza de datos es un objeto(object). Esto no es solo un concepto teórico: tiene implicaciones prácticas para cómo funcionan tus programas.

Cuando creas un número, una cadena(string), una lista(list) o cualquier otro valor, Python crea un objeto(object) en memoria. Un objeto es un contenedor que contiene:

  • Los datos reales (el valor(value))
  • Información sobre qué tipo(type) de datos es (el tipo(type))
  • Un identificador único (la identidad(identity))

Veamos esto en la práctica:

python
# Creando diferentes tipos de objetos
number = 42
text = "Hello"
items = [1, 2, 3]
 
# Cada una de estas variables se refiere a un objeto en memoria
print(number)  # Output: 42
print(text)    # Output: Hello
print(items)   # Output: [1, 2, 3]

Incluso valores simples como los enteros son objetos. Esto significa que tienen capacidades más allá de solo almacenar un número:

python
# Los enteros son objetos con métodos
number = 42
print(number.bit_length())  # Output: 6
 
# Las cadenas son objetos con métodos
text = "hello"
print(text.upper())  # Output: HELLO
 
# Las listas son objetos con métodos
items = [3, 1, 2]
items.sort()
print(items)  # Output: [1, 2, 3]

¿Por qué importa esto? Porque cuando asignas una variable o pasas datos a una función(function), no estás copiando el objeto: estás creando una referencia(reference) al mismo objeto. Esto es fundamentalmente diferente de cómo funcionan algunos otros lenguajes de programación, y comprender esta distinción evitará muchos bugs confusos.

python
# Creando un objeto lista
original = [1, 2, 3]
 
# Esto no crea una lista nueva: crea otra referencia
# a la MISMA lista
another_name = original
 
# Modificar mediante una referencia afecta a la otra
another_name.append(4)
 
print(original)      # Output: [1, 2, 3, 4]
print(another_name)  # Output: [1, 2, 3, 4]

Tanto original como another_name se refieren al mismo objeto lista en memoria. Cuando modificamos la lista mediante another_name, vemos el cambio mediante original porque ambos están mirando el mismo objeto.

Variable: original

Objeto lista: 1, 2, 3, 4

Variable: another_name

Este comportamiento se llama semántica de referencias(reference semantics), y es uno de los conceptos más importantes en la programación con Python. Lo exploraremos en profundidad a lo largo de este capítulo.

18.2) Identidad, tipo y valor de los objetos

Cada objeto en Python tiene tres características fundamentales que lo definen: identidad(identity), tipo(type) y valor(value). Comprender estas características te ayuda a razonar sobre cómo se comportan los objetos y cómo compararlos correctamente.

18.2.1) Identidad del objeto con id()

La identidad(identity) de un objeto es un número único que Python asigna cuando se crea el objeto. Esta identidad nunca cambia durante la vida del objeto: es como una dirección permanente en memoria.

Puedes obtener la identidad de un objeto usando la función id():

python
# Creando objetos y comprobando sus identidades
x = [1, 2, 3]
y = [1, 2, 3]
z = x
 
print(id(x))  # Output: 140234567890123 (example - actual number varies)
print(id(y))  # Output: 140234567890456 (different from x)
print(id(z))  # Output: 140234567890123 (same as x)

Los números reales que veas serán distintos cada vez que ejecutes el programa, pero el patrón se mantiene: x y y tienen identidades distintas porque son objetos diferentes, aunque contengan los mismos valores. Mientras tanto, z tiene la misma identidad que x porque z es solo otro nombre para el mismo objeto.

Aquí tienes un ejemplo práctico que muestra por qué importa la identidad:

python
# Dos estudiantes con las mismas calificaciones
student1_grades = [85, 90, 92]
student2_grades = [85, 90, 92]
 
# Estos son objetos diferentes (identidades diferentes)
print(id(student1_grades))  # Output: 140234567890123 (example)
print(id(student2_grades))  # Output: 140234567890456 (different)
 
# Modificar uno no afecta al otro
student1_grades.append(88)
print(student1_grades)  # Output: [85, 90, 92, 88]
print(student2_grades)  # Output: [85, 90, 92]

Ahora considera un escenario diferente:

python
# Las calificaciones de un estudiante seguidas por dos variables
original_grades = [85, 90, 92]
backup_reference = original_grades
 
# Estas se refieren al MISMO objeto (misma identidad)
print(id(original_grades))    # Output: 140234567890123 (example)
print(id(backup_reference))   # Output: 140234567890123 (same!)
 
# Modificar mediante cualquiera de los nombres afecta a ambos
backup_reference.append(88)
print(original_grades)     # Output: [85, 90, 92, 88]
print(backup_reference)    # Output: [85, 90, 92, 88]

Idea clave: Cuando dos variables tienen la misma identidad, se refieren exactamente al mismo objeto en memoria. Los cambios realizados mediante una variable son visibles mediante la otra porque solo hay un objeto que se está modificando.

18.2.2) Tipo del objeto con type()

El tipo(type) de un objeto determina qué clase de datos contiene y qué operaciones puedes realizar sobre él. Como aprendimos en el Capítulo 3, puedes comprobar el tipo de un objeto usando la función type():

python
# Diferentes tipos de objetos
number = 42
text = "Hello"
items = [1, 2, 3]
mapping = {"name": "Alice"}
 
print(type(number))   # Output: <class 'int'>
print(type(text))     # Output: <class 'str'>
print(type(items))    # Output: <class 'list'>
print(type(mapping))  # Output: <class 'dict'>

El tipo de un objeto nunca cambia después de su creación. No puedes convertir un entero en una cadena: solo puedes crear un nuevo objeto cadena basándote en el valor del entero:

python
# El tipo se fija al crearse
x = 42
print(type(x))  # Output: <class 'int'>
 
# Esto no cambia el tipo de x: crea un NUEVO objeto cadena
# y hace que x referencie ese nuevo objeto en su lugar
x = str(x)
# El objeto entero original (42) sigue existiendo en memoria hasta que el recolector de basura lo elimine
# x ahora apunta a un objeto completamente distinto: la cadena "42"
 
print(type(x))  # Output: <class 'str'>
print(x)        # Output: 42 (ahora es una cadena, no un entero)

Comprender los tipos es crucial porque distintos tipos admiten distintas operaciones:

python
# Las listas soportan append
grades = [85, 90]
grades.append(92)
print(grades)  # Output: [85, 90, 92]
 
# Las cadenas no tienen append: son inmutables
text = "Hello"
# text.append(" World")  # AttributeError: 'str' object has no attribute 'append'
 
# Pero las cadenas soportan concatenación
text = text + " World"
print(text)  # Output: Hello World

18.2.3) Valor del objeto

El valor(value) de un objeto son los datos reales que contiene. A diferencia de la identidad y el tipo, el valor puede cambiar para objetos mutables(mutable) (como listas y diccionarios) pero no puede cambiar para objetos inmutables(immutable) (como enteros y cadenas).

python
# Para objetos mutables, el valor puede cambiar
shopping_cart = ["milk", "bread"]
print(shopping_cart)  # Output: ['milk', 'bread']
 
shopping_cart.append("eggs")
print(shopping_cart)  # Output: ['milk', 'bread', 'eggs']
# Mismo objeto (misma identidad), valor diferente
 
# Para objetos inmutables, el valor no puede cambiar
count = 5
print(count)  # Output: 5
 
count = count + 1
print(count)  # Output: 6
# Esto creó un NUEVO objeto con una nueva identidad

Aquí tienes un ejemplo completo que muestra las tres características:

python
# Creando un objeto lista
data = [10, 20, 30]
 
print("Identity:", id(data))      # Output: Identity: 140234567890123 (example)
print("Type:", type(data))        # Output: Type: <class 'list'>
print("Value:", data)             # Output: Value: [10, 20, 30]
 
# Modificando el valor (la identidad y el tipo se mantienen iguales)
data.append(40)
 
print("Identity:", id(data))      # Output: Identity: 140234567890123 (unchanged)
print("Type:", type(data))        # Output: Type: <class 'list'> (unchanged)
print("Value:", data)             # Output: Value: [10, 20, 30, 40] (changed)

Objeto

Identidad: ID único

Tipo: class 'list'

Valor: 10, 20, 30, 40

Nunca cambia

Nunca cambia

Puede cambiar para tipos mutables

Comprender estas tres características te ayuda a predecir cómo se comportarán los objetos en tus programas. La identidad te dice si dos variables se refieren al mismo objeto, el tipo te dice qué operaciones están permitidas y el valor te dice qué datos contiene actualmente el objeto.

18.3) Tipos mutables e inmutables

Una de las distinciones más importantes en Python es entre tipos mutables(mutable) e inmutables(immutable). Esta distinción afecta cómo se comportan los objetos cuando intentas cambiarlos, y comprenderla previene muchos errores comunes de programación.

18.3.1) Tipos inmutables: valores que no pueden cambiar

Un objeto inmutable(immutable) es aquel cuyo valor no puede cambiar después de su creación. Cuando realizas una operación que parece modificar un objeto inmutable, Python en realidad crea un objeto nuevo con el valor modificado.

Los tipos inmutables de Python incluyen:

  • Enteros (int)
  • Números de punto flotante (float)
  • Cadenas (str)
  • Tuplas (tuple)
  • Booleanos (bool)
  • None (NoneType)

Veamos la inmutabilidad en acción con enteros:

python
# Creando un entero
x = 100
print("Original x:", x)           # Output: Original x: 100
print("Identity of x:", id(x))    # Output: Identity of x: 140234567890123 (example)
 
# Esto parece que estamos modificando x, pero en realidad estamos creando un objeto nuevo
x = x + 1
print("Modified x:", x)           # Output: Modified x: 101
print("Identity of x:", id(x))    # Output: Identity of x: 140234567890456 (different!)

La identidad cambió porque x = x + 1 creó un objeto entero completamente nuevo con el valor 101. El objeto original con valor 100 todavía existe (hasta que el recolector de basura de Python lo elimine), pero x ahora se refiere a un objeto diferente.

Las cadenas demuestran la inmutabilidad aún más claramente:

python
# Creando una cadena
message = "Hello"
print("Original:", message)        # Output: Original: Hello
print("Identity:", id(message))    # Output: Identity: 140234567890789 (example)
 
# Los métodos de cadena no modifican el original: devuelven nuevas cadenas
uppercase = message.upper()
print("Original:", message)        # Output: Original: Hello (unchanged)
print("Uppercase:", uppercase)     # Output: Uppercase: HELLO
print("Identity of original:", id(message))    # Output: Identity of original: 140234567890789 (same)
print("Identity of uppercase:", id(uppercase)) # Output: Identity of uppercase: 140234567891012 (different)

Incluso operaciones que parecen estar modificando una cadena en realidad crean nuevos objetos cadena:

python
# Construyendo una cadena con concatenación
text = "Python"
print("Before:", text, "- ID:", id(text))  # Output: Before: Python - ID: 140234567891234 (example)
 
text = text + " Programming"
print("After:", text, "- ID:", id(text))   # Output: After: Python Programming - ID: 140234567891567 (different)

Por qué importa la inmutabilidad: Los objetos inmutables son seguros de compartir entre diferentes partes de tu programa porque ninguna parte puede modificarlos accidentalmente. Esto hace que tu código sea más predecible y más fácil de razonar.

18.3.2) Tipos mutables: valores que pueden cambiar

Un objeto mutable(mutable) es aquel cuyo valor puede cambiar después de su creación sin crear un objeto nuevo. La identidad del objeto permanece igual, pero su contenido puede modificarse.

Los tipos mutables de Python incluyen:

  • Listas (list)
  • Diccionarios (dict)
  • Conjuntos (set)

Veamos la mutabilidad con listas:

python
# Creando una lista
numbers = [1, 2, 3]
print("Original:", numbers)        # Output: Original: [1, 2, 3]
print("Identity:", id(numbers))    # Output: Identity: 140234567892345 (example)
 
# Modificando la lista: mismo objeto, valor diferente
numbers.append(4)
print("Modified:", numbers)        # Output: Modified: [1, 2, 3, 4]
print("Identity:", id(numbers))    # Output: Identity: 140234567892345 (same!)

La identidad no cambió porque modificamos el objeto lista existente en lugar de crear uno nuevo. Esto es fundamentalmente diferente de cómo funcionan los tipos inmutables.

Los diccionarios y conjuntos también son mutables:

python
# Ejemplo de diccionario
student = {"name": "Alice", "grade": 85}
print("Before:", student, "- ID:", id(student))  # Output: Before: {'name': 'Alice', 'grade': 85} - ID: 140234567893012 (example)
 
student["grade"] = 90  # Modificando el diccionario
print("After:", student, "- ID:", id(student))   # Output: After: {'name': 'Alice', 'grade': 90} - ID: 140234567893012 (same)
 
# Ejemplo de conjunto
unique_numbers = {1, 2, 3}
print("Before:", unique_numbers, "- ID:", id(unique_numbers))  # Output: Before: {1, 2, 3} - ID: 140234567893345 (example)
 
unique_numbers.add(4)  # Modificando el conjunto
print("After:", unique_numbers, "- ID:", id(unique_numbers))   # Output: After: {1, 2, 3, 4} - ID: 140234567893345 (same)

18.3.3) Por qué importa la mutabilidad en la práctica

La diferencia entre tipos mutables e inmutables se vuelve crítica cuando múltiples variables se refieren al mismo objeto:

python
# Ejemplo inmutable: compartir con seguridad
x = "Hello"
y = x  # y se refiere al mismo objeto cadena
 
# "Modificar" x crea un objeto nuevo
x = x + " World"
 
print(x)  # Output: Hello World
print(y)  # Output: Hello (unchanged - y still refers to the original)
python
# Ejemplo mutable: modificaciones compartidas
list1 = [1, 2, 3]
list2 = list1  # list2 se refiere al MISMO objeto lista
 
# Modificar mediante list1 afecta a list2
list1.append(4)
 
print(list1)  # Output: [1, 2, 3, 4]
print(list2)  # Output: [1, 2, 3, 4] (also changed!)

Tipos inmutables

int, float, str, tuple, bool, None

El valor no puede cambiar

Las operaciones crean objetos nuevos

Seguro para compartir

Tipos mutables

list, dict, set

El valor puede cambiar

Las operaciones modifican el objeto existente

Compartir requiere precaución

Comprender la mutabilidad es esencial para:

  1. Predecir el comportamiento: Saber si una operación crea un objeto nuevo o modifica uno existente
  2. Evitar bugs: Prevenir modificaciones no intencionadas cuando los objetos se comparten
  3. Escribir código eficiente: Elegir el tipo correcto para tu caso de uso
  4. Comprender el comportamiento de las funciones: Saber cuándo los parámetros de una función pueden modificarse

En las siguientes secciones, exploraremos cómo funciona la asignación con estos distintos tipos y cómo crear copias independientes cuando sea necesario.

18.4) Cómo funciona la asignación con objetos

La asignación en Python no copia objetos: crea referencias(references) a objetos. Comprender esta distinción es crucial para escribir programas correctos, especialmente al trabajar con tipos mutables.

18.4.1) La asignación crea referencias, no copias

Cuando escribes x = y, Python no crea una copia del objeto al que se refiere y. En su lugar, hace que x se refiera al mismo objeto al que se refiere y. Ambas variables se convierten en nombres del mismo objeto en memoria.

Veamos esto primero con objetos inmutables:

python
# Asignación con enteros (inmutables)
a = 100
b = a  # b ahora se refiere al mismo objeto entero que a
 
print("a:", a)           # Output: a: 100
print("b:", b)           # Output: b: 100
print("Same object?", id(a) == id(b))  # Output: Same object? True
 
# "Modificar" a crea un objeto nuevo
a = a + 1
 
print("a:", a)           # Output: a: 101
print("b:", b)           # Output: b: 100 (unchanged)
print("Same object?", id(a) == id(b))  # Output: Same object? False

Con objetos inmutables, este comportamiento suele ser seguro porque no puedes modificar el objeto original. Cuando realizas una operación que cambia el valor, Python crea un objeto nuevo.

Sin embargo, con objetos mutables, el comportamiento es muy diferente:

python
# Asignación con listas (mutables)
list1 = [1, 2, 3]
list2 = list1  # list2 se refiere al MISMO objeto lista que list1
 
print("list1:", list1)   # Output: list1: [1, 2, 3]
print("list2:", list2)   # Output: list2: [1, 2, 3]
print("Same object?", id(list1) == id(list2))  # Output: Same object? True
 
# Modificar mediante list1 afecta a list2
list1.append(4)
 
print("list1:", list1)   # Output: list1: [1, 2, 3, 4]
print("list2:", list2)   # Output: list2: [1, 2, 3, 4] (also changed!)
print("Same object?", id(list1) == id(list2))  # Output: Same object? True

Tanto list1 como list2 son nombres para el mismo objeto lista. Cuando modificas la lista mediante cualquiera de los nombres, ves el cambio mediante ambos nombres porque solo hay una lista.

Asignación con tipos inmutables

Ambas variables se refieren al mismo objeto inicialmente

Las operaciones crean objetos nuevos

Las variables se vuelven independientes

Asignación con tipos mutables

Ambas variables se refieren al mismo objeto

Las operaciones modifican el objeto compartido

Los cambios son visibles a través de ambas variables

Aquí tienes un ejemplo práctico que muestra por qué esto importa:

python
# Gestionando las calificaciones de estudiantes
alice_grades = [85, 90, 92]
backup_grades = alice_grades  # Intentando crear una copia de seguridad
 
print("Original:", alice_grades)  # Output: Original: [85, 90, 92]
print("Backup:", backup_grades)   # Output: Backup: [85, 90, 92]
 
# Añadiendo una nueva calificación
alice_grades.append(88)
 
# ¡La "copia de seguridad" también se modificó!
print("Original:", alice_grades)  # Output: Original: [85, 90, 92, 88]
print("Backup:", backup_grades)   # Output: Backup: [85, 90, 92, 88]

Esto no es una copia de seguridad en absoluto: ambas variables se refieren a la misma lista. Para crear una copia de seguridad real, necesitas crear una copia (lo cubriremos en la Sección 18.8).

18.4.2) Asignación en llamadas a funciones

Cuando pasas un argumento a una función(function), Python usa la misma semántica de referencias. El parámetro se convierte en otro nombre para el mismo objeto:

python
# Función con parámetro inmutable
def increment(number):
    number = number + 1  # Crea un objeto nuevo
    return number
 
value = 5
result = increment(value)
 
print("Original value:", value)    # Output: Original value: 5 (unchanged)
print("Returned result:", result)  # Output: Returned result: 6

El parámetro number inicialmente se refiere al mismo objeto entero que value. Cuando hacemos number = number + 1, creamos un nuevo objeto entero y hacemos que number se refiera a él. El objeto original (y value) permanecen sin cambios.

Con objetos mutables, el comportamiento es diferente:

python
# Función con parámetro mutable
def add_item(items, new_item):
    items.append(new_item)  # Modifica la lista original
 
shopping_list = ["milk", "bread"]
add_item(shopping_list, "eggs")
 
print("Original list:", shopping_list)  # Output: Original list: ['milk', 'bread', 'eggs']

El parámetro items se refiere al mismo objeto lista que shopping_list. Cuando modificamos la lista mediante items, estamos modificando la lista original.

Aquí tienes un error común y cómo evitarlo:

python
# ERROR: Modificar el original sin querer
def process_grades(grades):
    grades.append(100)  # ¡Modifica el original!
    return grades
 
student_grades = [85, 90, 92]
processed = process_grades(student_grades)
 
print("Original:", student_grades)  # Output: Original: [85, 90, 92, 100] (modified!)
print("Processed:", processed)      # Output: Processed: [85, 90, 92, 100]
 
# CORRECTO: Crea una copia si no quieres modificar el original
def process_grades_safely(grades):
    # Crear una lista nueva con los mismos elementos
    result = grades + [100]  # La concatenación crea una lista nueva
    return result
 
student_grades = [85, 90, 92]
processed = process_grades_safely(student_grades)
 
print("Original:", student_grades)  # Output: Original: [85, 90, 92] (unchanged)
print("Processed:", processed)      # Output: Processed: [85, 90, 92, 100]

Nota importante sobre argumentos por defecto mutables: Un error común relacionado implica usar objetos mutables como valores por defecto de parámetros (como def func(items=[]):). Los parámetros por defecto se crean una vez cuando se define la función, no cada vez que se llama, lo cual puede llevar a un comportamiento inesperado donde la lista por defecto acumula valores a lo largo de múltiples llamadas a la función. Exploraremos esto en detalle en el Capítulo 20, pero ten presente que esta es una fuente frecuente de bugs al trabajar con parámetros mutables.

18.5) Semántica de referencias y aliasing de objetos

La semántica de referencias(reference semantics) significa que las variables en Python son nombres que se refieren a objetos, no contenedores que guardan valores. Cuando varias variables se refieren al mismo objeto, lo llamamos aliasing(aliasing). Comprender el aliasing es esencial para predecir cómo se comportan tus programas.

18.5.1) ¿Qué es el aliasing?

El aliasing(aliasing) ocurre cuando dos o más variables se refieren al mismo objeto en memoria. Las variables son “alias” entre sí: nombres distintos para lo mismo.

Veamos el aliasing con un ejemplo sencillo:

python
# Creando una lista y un alias
original = [1, 2, 3]
alias = original  # alias se refiere a la misma lista que original
 
print("Original:", original)  # Output: Original: [1, 2, 3]
print("Alias:", alias)        # Output: Alias: [1, 2, 3]
print("Same object?", id(original) == id(alias))  # Output: Same object? True
 
# Modificando mediante el alias
alias.append(4)
 
# El cambio es visible mediante ambos nombres
print("Original:", original)  # Output: Original: [1, 2, 3, 4]
print("Alias:", alias)        # Output: Alias: [1, 2, 3, 4]

Solo hay un objeto lista en memoria, pero tiene dos nombres: original y alias. Cualquier modificación realizada mediante cualquiera de los nombres afecta al mismo objeto subyacente.

Aquí tienes un ejemplo más realista con registros de estudiantes:

python
# Base de datos de estudiantes con aliasing
students = {
    "alice": {"name": "Alice", "grade": 85},
    "bob": {"name": "Bob", "grade": 90}
}
 
# Creando un alias al registro de Alice
alice_record = students["alice"]
 
print("Alice's grade:", alice_record["grade"])  # Output: Alice's grade: 85
 
# Modificando mediante el alias
alice_record["grade"] = 95
 
# El cambio es visible en el diccionario original
print("Updated grade:", students["alice"]["grade"])  # Output: Updated grade: 95

La variable alice_record es un alias del diccionario almacenado en students["alice"]. Cuando modificamos alice_record, estamos modificando el mismo diccionario que está almacenado en el diccionario students.

18.5.2) Detectar aliasing con el operador is

Puedes comprobar si dos variables son alias (se refieren al mismo objeto) usando el operador is:

python
# Comprobando aliasing
list1 = [1, 2, 3]
list2 = list1      # Alias
list3 = [1, 2, 3]  # Objeto diferente con el mismo valor
 
print("list1 is list2:", list1 is list2)  # Output: list1 is list2: True (aliases)
print("list1 is list3:", list1 is list3)  # Output: list1 is list3: False (different objects)
print("list1 == list3:", list1 == list3)  # Output: list1 == list3: True (same value)

El operador is comprueba la identidad (si dos variables se refieren al mismo objeto), mientras que == comprueba el valor (si dos objetos tienen el mismo contenido). Exploraremos esta distinción en detalle en la Sección 18.6.

18.5.3) Aliasing en colecciones

El aliasing se vuelve más complejo cuando los objetos se almacenan en colecciones:

python
# Creando una lista de listas
row = [0, 0, 0]
grid = [row, row, row]  # ¡Los tres elementos son alias de la misma lista!
 
print("Grid:")
for r in grid:
    print(r)
# Output:
# [0, 0, 0]
# [0, 0, 0]
# [0, 0, 0]
 
# Modificar un elemento afecta a todas las filas
grid[0][0] = 1
 
print("\nAfter modification:")
for r in grid:
    print(r)
# Output:
# [1, 0, 0]
# [1, 0, 0]
# [1, 0, 0]

Este es un error común al intentar crear una cuadrícula 2D. Las tres filas son alias de la misma lista, así que modificar una fila modifica todas.

La forma correcta de crear filas independientes:

python
# Creando filas independientes
grid = [[0, 0, 0], [0, 0, 0], [0, 0, 0]]  # Cada fila es una lista separada
 
print("Grid:")
for r in grid:
    print(r)
# Output:
# [0, 0, 0]
# [0, 0, 0]
# [0, 0, 0]
 
# Ahora modificar un elemento solo afecta a esa fila
grid[0][0] = 1
 
print("\nAfter modification:")
for r in grid:
    print(r)
# Output:
# [1, 0, 0]
# [0, 0, 0]
# [0, 0, 0]

18.6) Igualdad, identidad y pertenencia (==, is, e in) entre tipos

Python proporciona tres operadores fundamentales para comparar y comprobar relaciones entre objetos: == para igualdad, is para identidad, y in para pertenencia. Comprender cuándo usar cada operador es crucial para escribir programas correctos.

18.6.1) Igualdad con == (comparar valores)

El operador == comprueba si dos objetos tienen el mismo valor(value). No importa si son el mismo objeto en memoria: solo importa si su contenido es igual.

python
# Comparando valores con ==
list1 = [1, 2, 3]
list2 = [1, 2, 3]
list3 = list1
 
print(list1 == list2)  # Output: True (same values)
print(list1 == list3)  # Output: True (same values)

Aunque list1 y list2 son objetos diferentes en memoria, tienen el mismo valor, así que == devuelve True.

Así es como funciona == con diferentes tipos:

python
# Igualdad entre diferentes tipos
print(42 == 42)              # Output: True (same integer value)
print(42 == 42.0)            # Output: True (integer equals float with same value)
print("hello" == "hello")    # Output: True (same string value)
print([1, 2] == [1, 2])      # Output: True (same list contents)
print({"a": 1} == {"a": 1})  # Output: True (same dictionary contents)
 
# Valores diferentes
print(42 == 43)              # Output: False
print("hello" == "Hello")    # Output: False (case-sensitive)
print([1, 2] == [2, 1])      # Output: False (order matters)

Para colecciones, == realiza una comparación profunda(deep): comprueba si todos los elementos son iguales:

python
# Comparación profunda con estructuras anidadas
list1 = [[1, 2], [3, 4]]
list2 = [[1, 2], [3, 4]]
 
print(list1 == list2)  # Output: True (all nested elements are equal)
 
# Incluso si las listas internas son objetos diferentes
print(id(list1[0]) == id(list2[0]))  # Output: False (different objects)
print(list1[0] == list2[0])          # Output: True (same values)

18.6.2) Identidad con is (comparar identidad del objeto)

El operador is comprueba si dos variables se refieren al mismo objeto en memoria. Compara identidades, no valores.

python
# Comparando identidades con is
list1 = [1, 2, 3]
list2 = [1, 2, 3]
list3 = list1
 
print(list1 is list2)  # Output: False (different objects)
print(list1 is list3)  # Output: True (same object)
 
# Confirmando con id()
print(id(list1) == id(list2))  # Output: False
print(id(list1) == id(list3))  # Output: True

Cuándo usar is: El uso más común de is es para comprobar None:

python
# Comprobando None (la forma correcta)
def find_student(name, students):
    """Return student record or None if not found."""
    for student in students:
        if student["name"] == name:
            return student
    return None
 
students = [
    {"name": "Alice", "grade": 85},
    {"name": "Bob", "grade": 90}
]
 
result = find_student("Charlie", students)
 
# Usa 'is' para comprobar None
if result is None:
    print("Student not found")  # Output: Student not found
else:
    print(f"Found: {result}")

18.6.3) Pertenencia con in (comprobar contención)

El operador in comprueba si un valor está contenido en una colección. Funciona con cadenas, listas, tuplas, conjuntos y diccionarios:

python
# Pertenencia en diferentes tipos
print(2 in [1, 2, 3])           # Output: True
print("hello" in "hello world")  # Output: True
print("x" in {"x": 10, "y": 20}) # Output: True (checks keys)
print(5 in {1, 2, 3, 4, 5})     # Output: True

Para diccionarios, in comprueba si existe una clave:

python
# Comprobando pertenencia en diccionarios
student = {"name": "Alice", "grade": 85, "age": 20}
 
print("name" in student)    # Output: True (key exists)
print("Alice" in student)   # Output: False (value, not key)
print("grade" in student)   # Output: True (key exists)
 
# Comprobar valores requiere acceder a .values()
print("Alice" in student.values())  # Output: True

El operador not in comprueba ausencia:

python
# Comprobando ausencia
shopping_list = ["milk", "bread", "eggs"]
 
if "butter" not in shopping_list:
    print("Don't forget to buy butter!")  # Output: Don't forget to buy butter!

Resumen de cuándo usar cada operador:

  • Usa == cuando quieras comprobar si dos objetos tienen el mismo valor
  • Usa is cuando quieras comprobar si dos variables se refieren al mismo objeto (más comúnmente con None, o al depurar(aliasing))
  • Usa in cuando quieras comprobar si un valor está contenido en una colección

Comprender estas distinciones te ayuda a escribir comparaciones más precisas y correctas en tus programas.

18.7) Comparar objetos que contienen otros objetos

Cuando los objetos contienen otros objetos (como listas dentro de listas, o diccionarios que contienen listas), las comparaciones se vuelven más matizadas. Comprender cómo Python compara estructuras anidadas es esencial para trabajar con datos complejos.

18.7.1) Cómo funciona == con estructuras anidadas

El operador == realiza una comparación recursiva(recursive) para estructuras anidadas. No solo compara el contenedor externo, sino también todos los objetos anidados:

python
# Comparando listas anidadas
list1 = [[1, 2], [3, 4]]
list2 = [[1, 2], [3, 4]]
 
print(list1 == list2)  # Output: True
 
# Aunque las listas internas sean objetos diferentes
print(id(list1[0]) == id(list2[0]))  # Output: False
print(list1[0] == list2[0])          # Output: True

Python compara recursivamente cada elemento. Para que list1 == list2 sea True, cada elemento correspondiente debe ser igual, incluyendo los elementos anidados.

Aquí tienes un ejemplo más complejo:

python
# Estructura anidada con múltiples niveles
data1 = {
    "students": [
        {"name": "Alice", "grades": [85, 90, 92]},
        {"name": "Bob", "grades": [88, 91, 87]}
    ],
    "class": "Python 101"
}
 
data2 = {
    "students": [
        {"name": "Alice", "grades": [85, 90, 92]},
        {"name": "Bob", "grades": [88, 91, 87]}
    ],
    "class": "Python 101"
}
 
print(data1 == data2)  # Output: True

Python compara:

  1. Las claves y valores del diccionario en el nivel superior ("students" y "class")
  2. La lista de estudiantes
  3. Cada diccionario de estudiante (con claves "name" y "grades")
  4. La lista de calificaciones para cada estudiante
  5. Cada número de calificación individual

Todos los niveles deben coincidir para que la comparación devuelva True.

18.7.2) El orden importa para secuencias

Para secuencias (listas y tuplas), el orden de los elementos importa:

python
# El orden importa en listas
list1 = [[1, 2], [3, 4]]
list2 = [[3, 4], [1, 2]]
 
print(list1 == list2)  # Output: False (different order)
 
# Pero el orden no importa para conjuntos
set1 = {frozenset([1, 2]), frozenset([3, 4])}
set2 = {frozenset([3, 4]), frozenset([1, 2])}
 
print(set1 == set2)  # Output: True (sets are unordered)

18.7.3) Comparar colecciones de tipos diferentes

Diferentes tipos de colección (lista, tupla, conjunto) nunca son iguales entre sí, incluso si contienen los mismos elementos:

python
# Comparando tipos diferentes
print([1, 2, 3] == (1, 2, 3))  # Output: False (list vs tuple)
print([1, 2, 3] == {1, 2, 3})  # Output: False (list vs set)
 
# Incluso con los mismos elementos
list_version = [1, 2, 3]
tuple_version = (1, 2, 3)
set_version = {1, 2, 3}
 
print(list_version == tuple_version)  # Output: False
print(list_version == set_version)    # Output: False
print(tuple_version == set_version)   # Output: False

18.8) Copias superficiales de listas, diccionarios y conjuntos

Al trabajar con objetos mutables, a menudo necesitas crear copias independientes para evitar modificaciones no intencionadas. Por ejemplo, al hacer copias de seguridad de datos antes de procesarlos, crear escenarios de prueba sin afectar datos de producción, o pasar datos a funciones que no deberían modificar el original. Comprender cómo funcionan los mecanismos de copia de Python te ayuda a crear copias verdaderamente independientes cuando sea necesario.

Sin embargo, no todos los métodos de copia crean copias completamente independientes. Comprender la diferencia entre copias superficiales(shallow copies) y copias profundas(deep copies) es crucial para evitar bugs sutiles.

18.8.1) ¿Qué es una copia superficial?

Una copia superficial(shallow copy) crea un objeto nuevo, pero no crea copias de los objetos contenidos dentro de él. En su lugar, el nuevo objeto contiene referencias a los mismos objetos anidados que el original.

Veamos esto con una lista simple:

python
# Creando una copia superficial de una lista simple
original = [1, 2, 3]
copy = original.copy()  # Crea una copia superficial
 
print("Original:", original)  # Output: Original: [1, 2, 3]
print("Copy:", copy)          # Output: Copy: [1, 2, 3]
 
# Son objetos diferentes
print("Same object?", original is copy)  # Output: Same object? False
 
# Modificar la copia no afecta al original
copy.append(4)
 
print("Original:", original)  # Output: Original: [1, 2, 3]
print("Copy:", copy)          # Output: Copy: [1, 2, 3, 4]

Para listas simples que contienen objetos inmutables (como enteros), una copia superficial funciona perfectamente. La copia es independiente del original.

Pero ¿qué pasa con estructuras anidadas? Veamos dónde muestran sus limitaciones las copias superficiales:

python
# Copia superficial con listas anidadas
original = [[1, 2], [3, 4]]
copy = original.copy()
 
print("Original:", original)  # Output: Original: [[1, 2], [3, 4]]
print("Copy:", copy)          # Output: Copy: [[1, 2], [3, 4]]
 
# Las listas externas son objetos diferentes
print("Same outer list?", original is copy)  # Output: Same outer list? False
 
# Pero las listas anidadas son los MISMOS objetos
print("Same nested list?", original[0] is copy[0])  # Output: Same nested list? True
 
# Modificar una lista anidada afecta a ambas
copy[0].append(99)
 
print("Original:", original)  # Output: Original: [[1, 2, 99], [3, 4]]
print("Copy:", copy)          # Output: Copy: [[1, 2, 99], [3, 4]]

Lista original

Lista anidada 1: 1, 2, 99

Lista anidada 2: 3, 4

Copia superficial

18.8.2) Crear copias superficiales de listas

Hay varias formas de crear una copia superficial de una lista:

python
# Método 1: Usando el método copy()
original = [[1, 2], [3, 4]]
copy1 = original.copy()
 
# Método 2: Usando slicing de listas
copy2 = original[:]
 
# Método 3: Usando el constructor list()
copy3 = list(original)
 
# Las tres crean copias superficiales
print(copy1)  # Output: [[1, 2], [3, 4]]
print(copy2)  # Output: [[1, 2], [3, 4]]
print(copy3)  # Output: [[1, 2], [3, 4]]
 
# La lista externa es diferente
print(original is copy1)  # Output: False
print(original is copy2)  # Output: False
print(original is copy3)  # Output: False
 
# Pero las listas internas están COMPARTIDAS
print(original[0] is copy1[0])  # Output: True
print(original[0] is copy2[0])  # Output: True
print(original[0] is copy3[0])  # Output: True

18.8.3) Crear copias superficiales de diccionarios

Los diccionarios también soportan copia superficial:

python
# Método 1: Usando el método copy()
original = {"name": "Alice", "grade": 85}
copy1 = original.copy()
 
# Método 2: Usando el constructor dict()
copy2 = dict(original)
 
# Ambos crean copias superficiales
print(copy1)  # Output: {'name': 'Alice', 'grade': 85}
print(copy2)  # Output: {'name': 'Alice', 'grade': 85}
 
# Son objetos diferentes
print(original is copy1)  # Output: False
print(original is copy2)  # Output: False
 
# Modificar la copia no afecta al original
copy1["grade"] = 90
 
print("Original:", original)  # Output: Original: {'name': 'Alice', 'grade': 85}
print("Copy:", copy1)         # Output: Copy: {'name': 'Alice', 'grade': 90}

Sin embargo, con estructuras anidadas, se aplica la misma limitación de las copias superficiales:

python
# Copia superficial con diccionario anidado
original = {
    "name": "Alice",
    "grades": [85, 90, 92]
}
 
copy = original.copy()
 
print("Original:", original)  # Output: Original: {'name': 'Alice', 'grades': [85, 90, 92]}
print("Copy:", copy)          # Output: Copy: {'name': 'Alice', 'grades': [85, 90, 92]}
 
# Los diccionarios son objetos diferentes
print("Same dict?", original is copy)  # Output: Same dict? False
 
# Pero la lista de calificaciones es el MISMO objeto
print("Same grades list?", original["grades"] is copy["grades"])  # Output: Same grades list? True
 
# Modificar la lista de calificaciones afecta a ambos
copy["grades"].append(88)
 
print("Original:", original)  # Output: Original: {'name': 'Alice', 'grades': [85, 90, 92, 88]}
print("Copy:", copy)          # Output: Copy: {'name': 'Alice', 'grades': [85, 90, 92, 88]}
© 2025. Primesoft Co., Ltd.
support@primesoft.ai