La lección anterior terminó con tres preguntas sin responder. ¿Por qué pedir_opcion puede leer la constante PRIORIDADES sin que se la pasemos? ¿Qué pasaría si dentro de una función escribiéramos titulo = "otro": cambiaría el titulo del programa principal? ¿Y por qué la variable valor que vive dentro de pedir_texto no existe fuera de ella?
Las tres son la misma pregunta con distinta ropa: dónde vive cada nombre y quién puede verlo. Eso se llama ámbito (scope), y es una de esas ideas que, mientras no se entienden, producen errores desconcertantes: funciones que «no ven» una variable que está claramente escrita más arriba, cambios que se pierden sin dejar rastro, valores que aparecen donde no deberían.
Entender el ámbito no es un lujo teórico: es lo que convierte una función en una caja segura, algo que puedes usar sabiendo que no va a estropear nada de lo que hay fuera. Y es, además, la base de una regla de oro que aplicaremos en la lección siguiente: una función bien hecha depende solo de lo que recibe por parámetros.
Contenido
- Ámbito local: nacer y morir con la llamada
- Ámbito global
- La regla LEGB
- Leer una global no es lo mismo que asignarla
- La palabra
globaly por qué se desaconseja - Sombreado de nombres
- Cómo se pasan los argumentos en Python
- Constantes globales: la excepción aceptada
- Funciones anidadas y
nonlocal - Ámbito y depuración
- TareaFácil: auditoría de las funciones
- Errores comunes y consejos
- Ejercicios
- Conclusión
- Ámbito local: nacer y morir con la llamada
Toda variable creada dentro de una función —sea por asignación, sea por ser un parámetro— es local a esa función: existe mientras la llamada se ejecuta y desaparece en cuanto termina.
def calcular_coste(dias, tarifa):
subtotal = dias * tarifa
impuestos = subtotal * 0.21
return subtotal + impuestos
print(calcular_coste(4, 110.0))
print(subtotal)La primera línea funciona; la segunda no. subtotal e impuestos existieron, hicieron su trabajo y se esfumaron: fuera de la función Python no tiene ningún nombre llamado subtotal, y por eso responde con el NameError que ya conoces de 04-01. Esto, que parece una limitación, es en realidad la mejor propiedad de las funciones. Significa que puedes escribir una función usando los nombres que te dé la gana —i, valor, total— con la garantía de que no vas a pisar nada de lo que hay fuera. Sin ámbito local, cada nombre que usaras dentro de una función sería una mina para el resto del programa, y no podrías escribir ni cincuenta líneas sin colisionar contigo mismo. Funciona también entre funciones distintas: si pedir_titulo y pedir_prioridad usan las dos una variable llamada valor, no se estorban en absoluto, porque son dos variables distintas que casualmente comparten nombre, cada una en su propia caja.
- Ámbito global
El ámbito global es el del fichero: todo lo que se define en el margen izquierdo, fuera de cualquier función. Esas variables se llaman globales y son visibles desde cualquier punto del módulo, incluido el interior de las funciones.
NOMBRE_ESTUDIO = "Estudio Alba" # global
def mostrar_cabecera():
print(f"TareaFacil - {NOMBRE_ESTUDIO}") # lectura de una global: funciona
mostrar_cabecera() # TareaFacil - Estudio AlbaAquí está la respuesta a la primera pregunta de la lección: pedir_opcion podía leer PRIORIDADES porque las constantes son globales y las funciones pueden leer lo global sin pedir permiso.
Ahora bien, que se pueda no significa que convenga: una función que lee variables globales cambiantes deja de ser autónoma, porque su resultado ya no depende solo de lo que recibe sino del estado en que esté el programa en ese momento, lo que la vuelve impredecible al leerla e imposible de probar por separado. Volveremos sobre ello en la sección 8, con la única excepción razonable: las constantes.
- La regla LEGB
Cuando Python encuentra un nombre, lo busca en cuatro sitios y en este orden, quedándose con el primero que encuentre. La regla se conoce por sus iniciales en inglés: LEGB.
| Capa | Qué es | Ejemplo |
|---|---|---|
| L — Local | Dentro de la función que se está ejecutando | subtotal, un parámetro |
| E — Enclosing | La función que envuelve a esta, si la hay | ver sección 9 |
| G — Global | El nivel del fichero | NOMBRE_ESTUDIO, PRIORIDADES |
| B — Built-in | Lo que Python trae de serie | print, len, int, input |
flowchart TD
A["Local: dentro de la funcion"] --> B["Enclosing: funcion que la envuelve"]
B --> C["Global: el fichero"]
C --> D["Built-in: print, len, int, input"]
D --> E["No esta en ninguna capa: NameError"]
Léelo como una búsqueda que baja escalones: si el nombre está en la capa local, se usa ese y no se mira más abajo; si no, se prueba la envolvente; luego la global; luego las funciones integradas; y si tampoco está ahí, NameError. Una consecuencia inmediata y muy útil: el nombre más cercano gana. Si una función tiene una variable local total y además existe una global total, dentro de la función total se refiere siempre a la local. La global sigue existiendo, intacta, pero queda tapada mientras dure la llamada.
- Leer una global no es lo mismo que asignarla
Aquí está el punto que más confusión genera de toda la lección. Acabamos de ver que leer una global desde dentro de una función funciona sin más. Cambiemos una sola cosa: asignar en lugar de leer.
contador = 0
def incrementar():
contador = contador + 1 # intenta ASIGNAR
print(contador)
incrementar()Parece contradictorio, pero la regla es sencilla y no tiene excepciones: si en algún punto del cuerpo de una función hay una asignación a un nombre, ese nombre es local en toda la función. Python decide eso al compilar la función, antes de ejecutar nada. Así que contador es local dentro de incrementar, y la línea contador = contador + 1 intenta leer una local que todavía no tiene valor. De ahí el UnboundLocalError, que es un NameError especializado.
Y ojo con la variante que no da error, aún más peligrosa:
contador = 0
def poner_a_diez():
contador = 10 # crea una variable LOCAL nueva; la global no se toca
print(f"Dentro: {contador}")
poner_a_diez()
print(f"Fuera: {contador}")La función parece haber cambiado el contador, e incluso lo imprime cambiado, pero fuera todo sigue igual. Este es el error del que hablábamos: no falla, no avisa, y no hace lo que parece. Si alguna vez te preguntas «he asignado la variable dentro de la función y fuera no cambia», esta es la explicación.
- La palabra
global y por qué se desaconseja
global y por qué se desaconsejaPython ofrece una manera de forzar que la asignación afecte a la variable global: declararla con global al principio del cuerpo.
contador = 0
def incrementar():
global contador # "cuando diga contador, me refiero a la global"
contador = contador + 1
incrementar()
incrementar()
print(contador) # 2Funciona. Y aun así, la recomendación de prácticamente toda la comunidad Python es no usarlo salvo en casos muy contados. La razón se entiende mejor viendo lo que rompe:
total_horas = 0
def registrar_jornada(horas):
global total_horas
total_horas = total_horas + horas
def calcular_media(dias_trabajados):
global total_horas
total_horas = total_horas / dias_trabajados # destroza el acumulado
return total_horas
registrar_jornada(8)
registrar_jornada(6)
print(calcular_media(2)) # 7.0
registrar_jornada(5)
print(total_horas) # 12.0, un valor que ya no significa nadacalcular_media tenía un nombre inocente —«calcular»— y ha modificado el estado compartido. A partir de ahí, total_horas ya no son horas totales ni una media: es un número sin significado, y el programa seguirá funcionando sin quejarse. Imagina esto en un fichero de mil líneas con quince funciones que escriben la misma global: para saber cuánto vale una variable en un punto tendrías que leerte el programa entero.
Con global |
Con parámetros y return |
|
|---|---|---|
| ¿De qué depende el resultado? | Del estado del programa | Solo de los argumentos |
| ¿Se puede probar aislada? | No | Sí |
| ¿Quién puede romperla? | Cualquier función | Nadie más |
| ¿Se ve en la llamada lo que toca? | No | Sí |
La alternativa siempre es la misma: recibir por parámetro y devolver con return. La versión sana sería def registrar_jornada(total, horas): return total + horas, y que el programa principal guarde el resultado. Una línea más y un problema menos.
- Sombreado de nombres
Sombrear (shadowing) es declarar un nombre que tapa otro de una capa más externa. Ocurre continuamente y muchas veces es inofensivo, pero tiene una versión que hace daño de verdad: sombrear un built-in.
list = "Cartel feria del libro" # tapa la funcion integrada list()
print(list) # funciona: imprime el texto
print(list("abc")) # TypeError: 'str' object is not callableAl asignar a list, la capa global se ha quedado con ese nombre y la búsqueda LEGB ya no llega nunca a la capa built-in. El error, además, aparece lejos del punto donde se causó, a veces cientos de líneas después. Estos son los nombres que más se sombrean por accidente:
| Nombre integrado | Para qué sirve | Alternativa segura |
|---|---|---|
list |
Crear listas (módulo 5) | lista_tareas, elementos |
input |
Leer del teclado | entrada, texto_leido |
str, int, float |
Convertir tipos | texto, numero, importe |
type |
Consultar el tipo | tipo_tarea, categoria |
sum, max, min, len |
Cálculos sobre colecciones | total, mayor, menor |
Sombrear input es especialmente cruel en un programa como TareaFácil: escribes input = input("Titulo: ") una vez y la siguiente llamada a input(...) da TypeError: 'str' object is not callable, porque input ya no es la función sino la cadena que tecleaste. La defensa es simple: si tu editor colorea un nombre como si fuera especial, no lo uses para una variable.
- Cómo se pasan los argumentos en Python
Ya sabes que un argumento se asigna al parámetro. Pero ¿qué se asigna exactamente, el valor o «la variable»? En Python, lo que se pasa es la referencia al objeto: el parámetro pasa a apuntar al mismo objeto que el argumento. La consecuencia práctica, con los tipos que conoces —números, cadenas, booleanos, todos inmutables—, es tranquilizadora: reasignar el parámetro dentro no afecta a la variable de fuera.
def alargar_plazo(dias):
print(f" Recibo dias = {dias}")
dias = dias + 5 # reasigna el nombre LOCAL 'dias'
print(f" Dentro ahora dias = {dias}")
return dias
plazo = 4
resultado = alargar_plazo(plazo)
print(f"Fuera plazo = {plazo}, resultado = {resultado}")Traza de lo ocurrido: al llamar, el parámetro dias apunta al mismo objeto 4 que plazo. La línea dias = dias + 5 crea un objeto nuevo, 9, y hace que el nombre local dias apunte a él; plazo sigue apuntando a 4, porque el número 4 no ha cambiado —no puede: los enteros son inmutables. La única forma de que el exterior se entere del nuevo valor es el return.
Lo mismo pasa con las cadenas. Si una función normalizar(texto) hace texto = texto.strip().capitalize() y devuelve el resultado, la variable que pasaste desde fuera sigue con sus espacios y sus minúsculas intactos: es lo que viste en 02-01 sobre la inmutabilidad de las cadenas, ahora en el contexto de una llamada. Los métodos de cadena devuelven una cadena nueva; no modifican la original.
Un adelanto honesto: con objetos mutables —las listas del módulo 5— la historia tiene un capítulo más, porque una función sí puede modificar el contenido del objeto recibido y que el cambio se vea fuera. Lo que acabas de aprender sigue siendo cierto (reasignar el nombre nunca afecta al exterior), pero habrá que distinguir entre reasignar y modificar. Hoy, con números y cadenas, no hay ambigüedad posible.
- Constantes globales: la excepción aceptada
Después de tanta advertencia contra las globales, ¿por qué PRIORIDADES y EQUIPO viven en el nivel global y las leemos desde cualquier función sin remordimiento? Porque son constantes, y una constante no tiene los defectos de una global cambiante: no cambia nunca durante la ejecución, así que el resultado de la función sigue siendo predecible; se declara en un solo sitio, arriba del fichero, donde cualquiera la encuentra; se distingue a simple vista por el convenio de MAYÚSCULAS de 02-01; y documenta el dominio del problema, porque PRIORIDADES = ("alta", "media", "baja") dice qué prioridades existen en Estudio Alba, y eso es información del negocio, no estado del programa.
Constante global (ANCHO, EQUIPO) |
Variable global (titulo, contador) |
|
|---|---|---|
| ¿Cambia durante la ejecución? | No | Sí |
| ¿Quién la modifica? | Nadie | Cualquier función |
| ¿Complica razonar sobre el código? | No | Mucho |
| ¿Aceptable leerla desde una función? | Sí | Evítalo |
Un matiz de estilo: aunque leer una constante global sea legítimo, a veces conviene pasarla igualmente por parámetro. Es lo que hicimos con pedir_opcion(mensaje, opciones): pasándole las opciones, la función sirve para prioridades, para responsables y para lo que venga; leyendo PRIORIDADES directamente, solo serviría para prioridades. Regla práctica: si la constante forma parte de lo que la función hace, pásala; si es un detalle de presentación compartido por todo el programa, como ANCHO, léela.
- Funciones anidadas y
nonlocal
nonlocalFalta explicar la E de LEGB. En Python puedes definir una función dentro de otra, y la de dentro ve los nombres de la de fuera, que forman su ámbito envolvente (enclosing).
def preparar_informe(cliente):
encabezado = f"Informe para {cliente}"
def linea(texto):
return f"{encabezado} | {texto}" # lee 'encabezado' del ambito envolvente
print(linea("Tarea completada"))
preparar_informe("Panaderia Sole") # Informe para Panaderia Sole | Tarea completadaY si la función interna necesitara asignar a un nombre de la envolvente, existe la palabra nonlocal, hermana de global pero apuntando una capa más arriba en vez de al fichero entero. No la usaremos en el curso: con lo que sabes hoy, la solución limpia sigue siendo pasar el dato por parámetro y devolverlo. Basta con que reconozcas nonlocal cuando lo veas y sepas que se refiere a la capa envolvente. Las funciones anidadas sí volverán a aparecer, con un propósito muy concreto, en Funciones como valores.
- Ámbito y depuración
Hay una razón muy práctica para tomarse el ámbito en serio: el estado global disperso multiplica el tiempo que tardas en encontrar un error. Cuando una función depende solo de sus parámetros, diagnosticar un fallo es un problema cerrado: miras los argumentos, miras el valor devuelto y el error está dentro o no está. Cuando depende de globales que cualquiera puede modificar, la pregunta «¿por qué aquí total_horas vale 12?» no se responde mirando la función: hay que reconstruir toda la historia de la ejecución. Es la diferencia entre revisar una habitación y registrar un edificio.
Por eso, cuando algo no cuadre, la primera pregunta útil es: ¿de qué depende esta función? Si la respuesta es «solo de lo que recibe», ya has acotado el problema. Las técnicas concretas para investigar —trazas, puntos de interrupción, lectura de un traceback— son el tema de Depuración y manejo de errores; lo que el ámbito te da es un programa en el que esas técnicas funcionan rápido.
- TareaFácil: auditoría de las funciones
Toca revisar lo escrito en 04-02 con la pregunta de la sección anterior: ¿de qué depende cada función? Además de las cuatro que ya conoces, habíamos esbozado una quinta, mostrar_resumen(), para pintar la línea de estado de la tarea.
# La version defectuosa de mostrar_resumen
titulo = ""
prioridad = ""
dias = 0
def mostrar_resumen():
"""Pinta el resumen de la tarea... leyendo el estado global."""
print(f"{titulo} | {prioridad} | {dias} dias")| Función | Depende de | Veredicto |
|---|---|---|
pedir_texto(mensaje, obligatorio=True) |
Sus parámetros | Correcta |
pedir_opcion(mensaje, opciones) |
Sus parámetros | Correcta |
pedir_entero(mensaje, minimo, maximo) |
Sus parámetros | Correcta |
clasificar_urgencia(prioridad, dias) |
Sus parámetros | Correcta |
mostrar_resumen() |
Globales titulo, prioridad, dias |
Defectuosa |
Los cuatro primeros están limpios: puedes copiarlos a otro fichero y funcionan tal cual. mostrar_resumen() no: fuera de TareaFácil no sirve para nada, no se puede probar con datos inventados y, si mañana el programa gestionara dos tareas, no habría manera de decirle cuál pintar. La corrección es mecánica —convertir cada global leída en un parámetro:
def mostrar_resumen(titulo, prioridad, dias):
"""Pinta en una linea el resumen de la tarea indicada."""
print(f"{titulo} | {prioridad} | {dias} dias | {clasificar_urgencia(prioridad, dias)}")
mostrar_resumen("Cartel feria del libro", "alta", 2) # ... | 2 dias | CRITICA
mostrar_resumen("Menu Panaderia Sole", "baja", 9) # ... | 9 dias | NormalFíjate en las dos llamadas seguidas con datos distintos: eso, que ahora parece trivial, era imposible con la versión que leía globales. Y observa que mostrar_resumen sí llama a clasificar_urgencia, que es una función global: eso no es un problema de ámbito, porque las funciones, como las constantes, se definen una vez y no cambian. Con esto, tareafacil.py queda en un estado saneado: constantes globales arriba, funciones que dependen solo de sus parámetros, y el estado de la tarea todavía en variables sueltas del programa principal. Ese estado suelto es el último cabo que queda, y lo ataremos en la lección siguiente.
Errores Comunes y Consejos
Usar fuera una variable local. NameError. Lo que nace dentro de una función muere con ella: si necesitas ese valor fuera, devuélvelo con return.
Asignar a una global creyendo que la cambias. contador = 10 dentro de una función crea una local nueva y deja la global intacta, sin dar ningún error. Si el valor de fuera «no se actualiza», es esto.
UnboundLocalError. Aparece cuando lees un nombre que también asignas dentro de la función: la asignación lo ha hecho local en todo el cuerpo. Solución: recíbelo por parámetro y devuélvelo.
Sombrear un built-in. Llamar list, input, str o sum a una variable rompe el programa mucho después y en otro sitio. Añade una palabra: lista_tareas, texto_entrada. Y no abuses de global: funciona, y por eso tienta, pero cada global que escribes es una función que ya no puedes razonar por separado.
Consejo: haz la prueba del recorte. Copia una función a un fichero vacío. Si funciona sin arrastrar nada más, es autónoma; si le faltan nombres, esos nombres deberían ser parámetros. Y pon las constantes arriba y en MAYÚSCULAS: es la señal visual de «esto es global a propósito», y hace que cualquier otra global salte a la vista como sospechosa.
Ejercicios
Ejercicio 1: Predecir tres salidas
Di qué imprime cada bloque, y en su caso qué error se produce y por qué.
# Bloque A
x = 5
def f():
print(x)
f()
# Bloque B
y = 5
def g():
y = 99
print(y)
g()
print(y)
# Bloque C
z = 5
def h():
print(z)
z = 99
h()Ejercicio 2: Convertir globales en parámetros
Reescribe este programa para que ninguna función dependa de variables globales cambiantes, usando parámetros y return. Las constantes en MAYÚSCULAS pueden quedarse donde están.
TARIFA = 110.0
horas_acumuladas = 0
def anadir_horas(horas):
global horas_acumuladas
horas_acumuladas = horas_acumuladas + horas
def facturar():
global horas_acumuladas
return horas_acumuladas * (TARIFA / 8)
anadir_horas(8)
anadir_horas(4)
print(facturar())Ejercicio 3: Encontrar el sabotaje
Este programa imprime Hola y luego se rompe. Explica exactamente qué ocurre, en qué línea se causa el daño y en cuál se manifiesta, y arréglalo.
def saludar(texto):
print(texto)
saludar("Hola")
str = "Cartel feria del libro"
print(str)
saludar(str(2026))Soluciones
Solución 1.
| Bloque | Salida | Explicación |
|---|---|---|
| A | 5 |
La función solo lee la global: la búsqueda LEGB no la encuentra en local, baja a global y la usa |
| B | 99 y luego 5 |
La asignación crea una local y que tapa la global durante la llamada; la global no se toca |
| C | UnboundLocalError |
Hay una asignación a z en el cuerpo, así que z es local en toda la función, incluido el print anterior a la asignación |
El bloque C es el más instructivo: la línea que falla (print(z)) es anterior a la línea culpable (z = 99). Python decide qué nombres son locales al compilar la función, no al ejecutarla línea a línea.
Solución 2.
TARIFA = 110.0
def anadir_horas(acumuladas, horas):
"""Devuelve el nuevo total de horas tras anadir las indicadas."""
return acumuladas + horas
def facturar(acumuladas, tarifa=TARIFA):
"""Devuelve el importe correspondiente a las horas acumuladas."""
return acumuladas * (tarifa / 8)
horas = 0
horas = anadir_horas(horas, 8)
horas = anadir_horas(horas, 4)
print(f"{facturar(horas):.2f} EUR") # 165.00 EUREl acumulador sigue existiendo, pero ahora vive en el programa principal y viaja explícitamente por parámetros y valores devueltos: en cada línea se ve quién cambia qué. Además, facturar acepta una tarifa distinta si algún día hace falta, sin tocar la constante.
Solución 3. La línea str = "Cartel feria del libro" sombrea la función integrada str, dejando ese nombre apuntando a una cadena. Nada falla todavía: print(str) imprime el texto sin problema. El daño se manifiesta dos líneas después, en str(2026), que intenta llamar a una cadena como si fuera una función y produce TypeError: 'str' object is not callable. La corrección es renombrar la variable:
La moraleja es la distancia entre la causa y el síntoma: en un fichero largo, esas dos líneas podrían estar separadas por doscientas, y el mensaje de error apuntaría a la inocente.
Conclusión
Ya sabes dónde vive cada nombre. Las variables creadas dentro de una función son locales: nacen con la llamada, mueren al terminar y no existen fuera, lo que garantiza que una función no pise nada del resto del programa. Los nombres se buscan siguiendo la regla LEGB —local, envolvente, global, integrado—, y gana siempre el más cercano. Desde dentro de una función se puede leer una global sin más, pero asignarla crea una variable local nueva; forzar lo contrario con global funciona y casi nunca conviene, porque convierte cada función en algo que solo se entiende leyendo el programa entero. Sombrear nombres integrados como list o input produce errores lejos de donde se causaron. Y al pasar argumentos, Python entrega la referencia al objeto: con números y cadenas, inmutables, reasignar dentro nunca afecta a lo de fuera, así que la única vía de comunicación hacia el exterior es el return.
La regla que resume todo esto cabe en una línea: una función debería depender solo de sus parámetros, y comunicarse con el exterior solo por su valor devuelto. Las constantes en MAYÚSCULAS son la excepción aceptada, porque no cambian. Aplicándola, la auditoría de TareaFácil ha dejado limpias las cuatro funciones de validación y ha corregido mostrar_resumen(), que leía el estado global y ahora recibe lo que necesita. Tienes ya las tres piezas de la ingeniería de funciones: definir y llamar (04-01), parámetros y return (04-02) y ámbito (04-03). Lo que falta es el criterio para usarlas a escala: cómo partir un programa entero de cien líneas en funciones del tamaño adecuado, cómo decidir qué va en cada una y en qué orden escribirlas. Eso es Descomponer un programa en funciones, donde TareaFácil pasará por fin de la v0.6 a la v0.7.
Fundamentos de la Programación
Módulo 1: Introducción a la Programación
- ¿Qué es la programación?
- Historia de la programación
- Lenguajes de programación
- Entornos de desarrollo
- Del problema al algoritmo
Módulo 2: Conceptos Básicos
- Variables y tipos de datos
- Operadores y expresiones
- Entrada y salida de datos
- Conversión de tipos y validación de datos
Módulo 3: Estructuras de Control
Módulo 4: Funciones y Procedimientos
- Definición y uso de funciones
- Parámetros y retorno de valores
- Ámbito de variables
- Descomponer un programa en funciones
- Funciones como valores: lambda y orden superior
Módulo 5: Estructuras de Datos
- Listas y arreglos
- Cadenas de caracteres
- Diccionarios y conjuntos
- Tuplas y estructuras anidadas
- Guardar datos en archivos: texto, CSV y JSON
Módulo 6: Algoritmos Básicos
Módulo 7: Objetos y Organización del Código
- De los datos a los objetos: clases e instancias
- Atributos, métodos y constructor
- Colecciones de objetos
- Módulos, paquetes e importaciones
Módulo 8: Buenas Prácticas y Herramientas
- Documentación y comentarios
- Depuración y manejo de errores
- Control de versiones
- Pruebas automatizadas
- Estilo, legibilidad y refactorización
