2.16. Ámbito

Cuando Python busca un nombre dentro de una función, recorre una secuencia específica de ámbitos. Entender esa secuencia explica por qué algunas asignaciones ocultan nombres externos, por qué otras los modifican y por qué las funciones anidadas pueden recordar valores del lugar donde se definieron.

Cajas anidadas que muestran un ámbito local dentro de un ámbito de módulo dentro del ámbito de las funciones integradas, con una flecha que indica que la búsqueda de nombres avanza hacia afuera desde el marco más interno.

La búsqueda de nombres comienza en el ámbito local de la función y avanza hacia afuera hasta los ámbitos del módulo y de las funciones integradas hasta encontrar una coincidencia.

2.16.1. Ámbito local y de módulo

Los nombres definidos dentro de una función son locales a esa función y desaparecen cuando la llamada termina:

def f():
    x = 10
    print(x)

f()
print(x)              # NameError: x is not defined

Los nombres definidos en el nivel superior de un archivo .py son de nivel de módulo (a veces llamados globales) y son visibles en todas partes de ese archivo, incluso dentro de las funciones:

CAMERA = "OpenMV"

def banner():
    print("running on", CAMERA)

Leer un nombre de nivel de módulo desde dentro de una función es automático. Asignar a ese nombre desde dentro de una función no lo es: la asignación crea una nueva variable local que oculta la de nivel de módulo durante el resto de la llamada:

counter = 0

def bump():
    counter = counter + 1     # UnboundLocalError

El counter del lado izquierdo convierte a counter en un nombre local dentro de bump, por lo que la lectura del lado derecho no tiene ningún valor que encontrar.

2.16.1.1. La palabra clave global

Para reasignar realmente un nombre de nivel de módulo desde dentro de una función, decláralo primero como global:

counter = 0

def bump():
    global counter
    counter += 1

Recurre a global con moderación. Las funciones que mutan estado oculto son más difíciles de razonar que las funciones que toman valores como argumentos y devuelven valores nuevos. La solución habitual para «necesito compartir estado» es pasar un objeto (una lista, un diccionario, una instancia de clase) como argumento y mutarlo a él en su lugar.

2.16.2. Lambdas

Una lambda construye una pequeña función anónima en una sola expresión:

square = lambda x: x * x
square(7)             # 49

Es exactamente equivalente a

def square(x):
    return x * x

El cuerpo de una lambda debe ser una sola expresión: ni instrucciones, ni varias líneas. Su uso principal es pasar una función diminuta como argumento a algo que toma una función:

pairs = [("b", 2), ("a", 3), ("c", 1)]
pairs.sort(key=lambda item: item[1])
# [('c', 1), ('b', 2), ('a', 3)]

Cuando el cuerpo crece más allá de una expresión, cambia a un def real. Nombrar una función con def también le da un nombre en los rastreos (tracebacks), que una lambda no tiene.

2.16.3. Clausuras

Una función definida dentro de otra función puede leer nombres del ámbito de la función que la contiene. La función interna captura esos nombres y sigue funcionando incluso después de que la llamada externa haya retornado:

def make_adder(n):
    def add(x):
        return x + n
    return add

add5 = make_adder(5)
add10 = make_adder(10)
print(add5(100), add10(100))

Salida:

105 110

add5 y add10 son dos funciones separadas, cada una recordando su propio n. A una función que construye y devuelve de esta manera una función interna personalizada se le llama clausura. Es la principal razón por la que un lenguaje necesita funciones anidadas en primer lugar: una forma de incorporar cierto estado en un valor de función y luego entregar ese valor como un único objeto invocable.

Leer nombres capturados ocurre automáticamente. Reasignar uno necesita una palabra clave adicional. El ejemplo de abajo hace lo que parece correcto y falla:

def make_counter():
    count = 0
    def tick():
        count = count + 1     # UnboundLocalError
        return count
    return tick

La asignación a count dentro de tick convierte a count en local de tick, igual que lo habría hecho local en una función de nivel superior. La palabra clave nonlocal le indica a Python «este nombre vive en la función que la contiene, reasígnalo allí»:

def make_counter():
    count = 0
    def tick():
        nonlocal count
        count += 1
        return count
    return tick

c = make_counter()
print(c(), c(), c())

Salida:

1 2 3

nonlocal es para el ámbito de la función contenedora lo que global es para el ámbito del módulo. Ten en cuenta que mutar un objeto capturado (llamar a some_list.append(...), some_dict[k] = v) no necesita nonlocal: el nombre no se reasigna, solo se cambia el objeto al que apunta.