Curso de Python / Entender ámbitos, alias y objetos mutables

El ámbito determina dónde Python busca un nombre y la mutabilidad determina qué cambios ven los nombres que comparten un objeto. Separar la asignación de la modificación de objetos permite explicar muchos cambios inesperados después de una llamada a función.

Separar nombres locales y nombres externos

Un nombre al que se asigna un valor dentro de una función normalmente es local, salvo que se declare lo contrario. Leer un valor externo no es lo mismo que volver a asociar su nombre con otro valor. Pasar entradas como argumentos y devolver resultados suele facilitar las pruebas frente a depender de variables globales.

En la búsqueda ordinaria de nombres de funciones se distingue el ámbito local, las funciones contenedoras, el módulo global y los nombres incorporados: LEGB por sus iniciales en inglés. global hace referencia a una asociación del módulo y nonlocal a una de una función contenedora.

Antes de añadir estas declaraciones, considera si un flujo explícito de argumentos y resultados expresaría mejor la operación. La guía de ámbitos y espacios de nombres describe el modelo.

Observar listas compartidas y copias superficiales

Guarda el primer programa como scope.py en UTF-8. Ejecuta python scope.py en la carpeta de práctica con el entorno virtual seleccionado; utiliza python3 fuera del entorno en macOS o Ubuntu cuando corresponda.

def add_fee(items, fee):
    items.append(fee)

original = [3500, 8000]
alias = original
copy = original.copy()
add_fee(alias, 500)
print(original)
print(copy)

nested = [[1], [2]]
shallow = nested.copy()
shallow[0].append(9)
print(nested)

La salida esperada es:

[3500, 8000, 500]
[3500, 8000]
[[1, 9], [2]]

alias = original crea otro nombre para la misma lista. copy() crea una lista exterior nueva, por lo que añadir 500 al alias no modifica esa copia. Sin embargo, en la estructura anidada la copia exterior conserva las mismas listas interiores: modificar shallow[0] también se observa a través de nested.

copy.deepcopy() puede copiar estructuras recursivamente, pero no es una solución universal para duplicar archivos, conexiones abiertas o estado que debe compartirse. Reconstruye los datos que necesitan independencia o utiliza valores inmutables si expresan mejor el contrato.

Comparar nombres locales y una celda nonlocal

Guarda este segundo ejemplo completo como scope_names.py y ejecuta python scope_names.py. Así puedes conservar el primer archivo mientras comparas los dos comportamientos.

value = 10
def local_read():
    value = 20
    return value
def broken():
    value += 1
    return value
print(local_read(), value)
try:
    broken()
except UnboundLocalError:
    print("local name not initialized")
def make_counter():
    count = 0
    def increment():
        nonlocal count
        count += 1
        return count
    return increment
counter = make_counter()
print(counter(), counter())

La salida esperada muestra el valor local, el del módulo, el fallo de lectura anterior a la asignación y el contador compartido por llamadas sucesivas:

20 10
local name not initialized
1 2

La asignación de value dentro de broken() hace que el nombre sea local. La operación += intenta leerlo antes de que tenga un valor local y produce UnboundLocalError. En cambio, nonlocal count permite actualizar la asociación de la función contenedora.

Consulta la referencia de resolución de nombres para precisar las condiciones.

Documentar si una función modifica sus entradas

Quien llama a una función debe poder saber si esta cambia una lista recibida. Para conservarla, la función puede construir y devolver un resultado nuevo. En las pruebas, comprueba tanto el resultado como los datos originales: un total correcto no demuestra que no haya efectos secundarios.

  1. Llama a add_fee(copy, 100) y comprueba que original no recibe ese 100.
  2. Sustituye append dentro de la función por items = items + [fee] y observa que la lista externa no cambia.
  3. Restaura el ejemplo y explica por separado la independencia del contenedor exterior y la de sus elementos anidados.

Los argumentos predeterminados muestran otra forma de compartir accidentalmente un objeto entre llamadas. La misma distinción ayuda a entender los atributos de instancia y de clase.