Historia de la pregunta:
El paso de parámetros en Python implementa el principio "call by object reference" (a veces se le llama call by sharing). Esto significa que la variable dentro de la función comienza a apuntar al mismo objeto en la memoria que el argumento pasado desde afuera.
Problema:
Si la función modifica el objeto mutable pasado (por ejemplo, una lista o un diccionario), los cambios son visibles también fuera de la función. Esto puede llevar a errores difíciles de detectar, especialmente si se espera que la función no modifique los datos de entrada.
Solución:
Para evitar efectos secundarios, se debe hacer una copia del objeto dentro de la función o utilizar estructuras de datos inmutables. Para copiar, se aplican métodos estándar (por ejemplo, list.copy() para listas, dict.copy() para diccionarios o copy.deepcopy()).
Ejemplo de código:
def append_one(xs): xs.append(1) return xs lst = [0] append_one(lst) print(lst) # [0, 1] # ¿Cómo evitar cambios? Hacer una copia: def safe_append_one(xs): ys = xs.copy() ys.append(1) return ys lst2 = [0] safe_append_one(lst2) print(lst2) # [0]
Características clave:
¿Se puede estar seguro de que una copia de la lista a través de .copy() es completamente independiente de la lista original?
No: .copy() crea una copia superficial. Si hay objetos mutables anidados dentro, los cambios en ellos serán visibles también en el original.
import copy lst = [[1, 2], [3, 4]] shallow = lst.copy() shallow[0][0] = 42 print(lst) # [[42, 2], [3, 4]] deep = copy.deepcopy(lst) deep[0][0] = 100 print(lst) # [[42, 2], [3, 4]]
¿Es el retorno de un nuevo objeto basado en el de entrada una garantía de ausencia de cambios en el original?
No siempre. Si en el nuevo objeto se utilizan partes del original (por ejemplo, una referencia a una lista anidada interna), el objeto original puede modificarse.
def duplicate_list(xs): return xs * 2 lst = [[1], [2]] res = duplicate_list(lst) res[0][0] = 999 print(lst) # [[999], [2]]
¿Pueden los argumentos de valores por defecto para objetos mutables causar problemas en llamamientos múltiples a la función?
Sí: el valor por defecto se evalúa solo una vez al definir la función.
def add_item(item, container=[]): container.append(item) return container print(add_item(1)) # [1] print(add_item(2)) # [1, 2]
Caso negativo
En la biblioteca de procesamiento de configuraciones, se utilizó una lista como valor por defecto, lo que provocó la acumulación de elementos entre diferentes llamamientos a la función. El comportamiento era impredecible y se identificó muy tarde.
Pros:
Menos código para las llamadas repetidas, visible ahorro de memoria.
Contras:
Comportamiento implícito, dificultades para depurar, errores a largo plazo.
Caso positivo
Uso de None como valor por defecto y creación explícita de un nuevo objeto en cada llamada.
Pros:
Previsibilidad, ausencia de efectos secundarios inesperados, fiabilidad.
Contras:
Requiere conciencia y un poco más de código.