Capítulo 9: Introspección y Atributos
Capítulo 9: Introspección y Atributos
“El código que puede verse a sí mismo es código poderoso.” — Anónimo
La introspección (o reflexión) es la capacidad de un programa para examinar y modificar su propia estructura en runtime. Lua provee herramientas poderosas para esto, principalmente a través de la biblioteca debug y el concepto de _ENV.
La Biblioteca debug
La biblioteca debug es tu ventana al interior de Lua. Úsala con cuidado: es poderosa pero puede romper encapsulación.
ADVERTENCIA
La librería
debugno debería usarse en código de producción excepto para logging/debugging. Puede violar encapsulación y tener impacto en performance.
Información de Funciones
local function greet(name)
return "Hello, " .. name
end
-- >>> local info = debug.getinfo(greet)
-- >>> for k, v in pairs(info) do
-- >>> print(k, v)
-- >>> end
-- source @script.lua
-- short_src script.lua
-- linedefined 1
-- lastlinedefined 3
-- what Lua
-- nparams 1
-- isvararg false
Campos importantes:
source: Archivo donde se definiólinedefined: Línea de inicionparams: Número de parámetrosisvararg: Si acepta varargs (...)what: “Lua”, “C”, o “main”
Obtener Nombre de Función
local function get_function_name(fn)
local info = debug.getinfo(fn, "n")
return info.name or "anonymous"
end
-- >>> print(get_function_name(print))
-- print
-- >>> print(get_function_name(function() end))
-- anonymous
Stack Traces
local function print_stack_trace()
local level = 2 -- Empezar desde el caller
while true do
local info = debug.getinfo(level, "Snl")
if not info then break end
print(string.format(" [%d] %s:%d in function '%s'",
level - 1,
info.short_src,
info.currentline or 0,
info.name or "?"
))
level = level + 1
end
end
local function foo()
print_stack_trace()
end
local function bar()
foo()
end
-- >>> bar()
-- [1] script.lua:15 in function 'foo'
-- [2] script.lua:19 in function 'bar'
-- [3] script.lua:22 in function '?'
Inspeccionar Variables Locales
local function inspect_locals()
local level = 2 -- El caller
local i = 1
while true do
local name, value = debug.getlocal(level, i)
if not name then break end
print(string.format(" %s = %s", name, tostring(value)))
i = i + 1
end
end
local function demo(x, y)
local z = x + y
inspect_locals()
return z
end
-- >>> demo(5, 3)
-- x = 5
-- y = 3
-- z = 8
Modificar Variables Locales
local function modify_local(level, name, new_value)
local i = 1
while true do
local var_name, var_value = debug.getlocal(level, i)
if not var_name then break end
if var_name == name then
debug.setlocal(level, i, new_value)
return true
end
i = i + 1
end
return false
end
-- >>> local function test()
-- >>> local x = 10
-- >>> print("Before:", x)
-- >>> modify_local(2, "x", 99) -- Nivel 2 = esta función
-- >>> print("After:", x)
-- >>> end
-- >>> test()
-- Before: 10
-- After: 99
Upvalues: Inspeccionar Closures
Los upvalues son variables capturadas por closures. Podemos inspeccionarlos:
local function make_counter()
local count = 0 -- upvalue
return function()
count = count + 1
return count
end
end
local counter = make_counter()
-- >>> local name, value = debug.getupvalue(counter, 1)
-- >>> print(name, value)
-- count 0
-- >>> counter()
-- >>> name, value = debug.getupvalue(counter, 1)
-- >>> print(name, value)
-- count 1
Modificar Upvalues
-- >>> debug.setupvalue(counter, 1, 100)
-- >>> print(counter())
-- 101
Listar Todos los Upvalues
local function list_upvalues(fn)
local i = 1
local upvalues = {}
while true do
local name, value = debug.getupvalue(fn, i)
if not name then break end
upvalues[name] = value
i = i + 1
end
return upvalues
end
-- >>> local function factory(x, y)
-- >>> return function() return x + y end
-- >>> end
-- >>> local fn = factory(10, 20)
-- >>> local ups = list_upvalues(fn)
-- >>> for name, value in pairs(ups) do
-- >>> print(name, value)
-- >>> end
-- x 10
-- y 20
El Global _ENV
_ENV es una variable especial que contiene el ambiente actual (las variables globales).
Inspeccionar Globales
-- >>> for name, value in pairs(_ENV) do
-- >>> if type(value) == "function" then
-- >>> print(name)
-- >>> end
-- >>> end
-- print
-- pairs
-- ipairs
-- tonumber
-- tostring
-- ... (todas las funciones globales)
Crear Ambientes Aislados
local function run_sandboxed(code, sandbox_env)
sandbox_env = sandbox_env or {}
-- Crear ambiente con acceso limitado
local safe_env = {
print = print,
tostring = tostring,
tonumber = tonumber,
type = type,
pairs = pairs,
ipairs = ipairs,
-- NO incluir: io, os, debug, dofile, loadfile, require
}
-- Merge con sandbox_env
for k, v in pairs(sandbox_env) do
safe_env[k] = v
end
-- Cargar código con ambiente custom
local fn, err = load(code, "sandbox", "t", safe_env)
if not fn then
return nil, err
end
return pcall(fn)
end
-- >>> local ok, result = run_sandboxed([[
-- >>> local x = 10
-- >>> local y = 20
-- >>> return x + y
-- >>> ]])
-- >>> print(ok, result)
-- true 30
-- >>> ok, result = run_sandboxed([[
-- >>> os.execute("rm -rf /") -- ¡Peligroso!
-- >>> ]])
-- >>> print(ok, result)
-- false attempt to index a nil value (global 'os')
Caso Práctico: Pretty Printer
Imprimir estructuras de datos de forma legible:
local function pretty_print(value, indent)
indent = indent or 0
local indent_str = string.rep(" ", indent)
if type(value) == "table" then
print(indent_str .. "{")
for k, v in pairs(value) do
if type(k) == "string" then
io.write(indent_str .. " " .. k .. " = ")
else
io.write(indent_str .. " [" .. tostring(k) .. "] = ")
end
if type(v) == "table" then
print("")
pretty_print(v, indent + 1)
else
print(tostring(v))
end
end
print(indent_str .. "}")
else
print(indent_str .. tostring(value))
end
end
-- >>> local data = {
-- >>> name = "Alice",
-- >>> age = 30,
-- >>> address = {
-- >>> city = "Madrid",
-- >>> country = "Spain"
-- >>> },
-- >>> hobbies = {"reading", "coding"}
-- >>> }
-- >>> pretty_print(data)
-- {
-- name = Alice
-- age = 30
-- address =
-- {
-- city = Madrid
-- country = Spain
-- }
-- hobbies =
-- {
-- [1] = reading
-- [2] = coding
-- }
-- }
Versión con Detección de Ciclos
local function pretty_print_safe(value, indent, seen)
indent = indent or 0
seen = seen or {}
local indent_str = string.rep(" ", indent)
if type(value) == "table" then
if seen[value] then
print(indent_str .. "<circular reference>")
return
end
seen[value] = true
print(indent_str .. "{")
for k, v in pairs(value) do
local key_str = type(k) == "string" and k or "[" .. tostring(k) .. "]"
io.write(indent_str .. " " .. key_str .. " = ")
if type(v) == "table" then
print("")
pretty_print_safe(v, indent + 1, seen)
else
print(tostring(v))
end
end
print(indent_str .. "}")
else
print(indent_str .. tostring(value))
end
end
Caso Práctico: Profiler Simple
Medir tiempo de ejecución de funciones automáticamente:
local Profiler = {}
function Profiler.new()
local self = {
stats = {},
active = true
}
-- Hook que se llama en cada llamada a función
local function hook(event)
if not self.active then return end
local info = debug.getinfo(2, "nS")
if not info or info.what ~= "Lua" then return end
local name = info.name or (info.source .. ":" .. info.linedefined)
if event == "call" then
if not self.stats[name] then
self.stats[name] = {
calls = 0,
total_time = 0,
start_time = nil
}
end
self.stats[name].calls = self.stats[name].calls + 1
self.stats[name].start_time = os.clock()
elseif event == "return" then
if self.stats[name] and self.stats[name].start_time then
local elapsed = os.clock() - self.stats[name].start_time
self.stats[name].total_time = self.stats[name].total_time + elapsed
self.stats[name].start_time = nil
end
end
end
function self.start()
debug.sethook(hook, "cr")
end
function self.stop()
debug.sethook()
end
function self.report()
print("\nProfiling Report:")
print(string.format("%-30s %10s %15s %15s", "Function", "Calls", "Total Time", "Avg Time"))
print(string.rep("-", 70))
local sorted = {}
for name, stat in pairs(self.stats) do
table.insert(sorted, {name = name, stat = stat})
end
table.sort(sorted, function(a, b)
return a.stat.total_time > b.stat.total_time
end)
for _, item in ipairs(sorted) do
local avg_time = item.stat.total_time / item.stat.calls
print(string.format("%-30s %10d %15.6f %15.6f",
item.name,
item.stat.calls,
item.stat.total_time,
avg_time))
end
end
return self
end
-- Uso:
-- >>> local profiler = Profiler.new()
-- >>> profiler.start()
-- >>> -- Tu código aquí
-- >>> profiler.stop()
-- >>> profiler.report()
Caso Práctico: Debugger Simple
local Debugger = {}
function Debugger.new()
local self = {
breakpoints = {},
step_mode = false
}
function self.add_breakpoint(file, line)
if not self.breakpoints[file] then
self.breakpoints[file] = {}
end
self.breakpoints[file][line] = true
end
local function hook(event, line)
local info = debug.getinfo(2, "Sl")
if not info then return end
local should_break = false
-- Check breakpoint
if self.breakpoints[info.source] and
self.breakpoints[info.source][line] then
should_break = true
end
-- Step mode
if self.step_mode then
should_break = true
end
if should_break then
print(string.format("\n[DEBUGGER] Stopped at %s:%d",
info.short_src, line))
print("Commands: (c)ontinue, (s)tep, (l)ocals, (q)uit")
while true do
io.write("> ")
local cmd = io.read()
if cmd == "c" then
self.step_mode = false
break
elseif cmd == "s" then
self.step_mode = true
break
elseif cmd == "l" then
local i = 1
while true do
local name, value = debug.getlocal(2, i)
if not name then break end
print(string.format(" %s = %s", name, tostring(value)))
i = i + 1
end
elseif cmd == "q" then
debug.sethook()
error("Debugger stopped")
end
end
end
end
function self.start()
debug.sethook(hook, "l")
end
function self.stop()
debug.sethook()
end
return self
end
Function Attributes (Lua 5.4+)
En Lua 5.4+, las variables locales pueden tener atributos:
-- Variable constante
local x <const> = 10
-- x = 20 -- Error: attempt to assign to const variable
-- Variable "to-be-closed" (se cierra automáticamente al salir del scope)
do
local file <close> = io.open("test.txt", "w")
file:write("Hello")
-- No necesitas file:close(), se cierra automáticamente
end
DEEP DIVE: Cómo Funciona _ENV
Cada chunk de código en Lua tiene su propio _ENV. Cuando accedes a una variable global, realmente estás accediendo a _ENV.variable:
-- Esto:
print("Hello")
-- Es realmente:
_ENV.print("Hello")
Puedes cambiar _ENV para cambiar el ambiente:
local old_env = _ENV
local new_env = {
print = function(msg)
old_env.print("[CUSTOM] " .. msg)
end
}
_ENV = new_env
-- >>> print("Hello")
-- [CUSTOM] Hello
Warnings y Best Practices
- No uses
debugen producción excepto para logging _ENVes poderoso pero peligroso: Úsalo con cuidado- Sandboxing es difícil: Aislar código no confiable requiere cuidado extremo
- Performance: Hooks de debug tienen impacto en performance
SOAPBOX: Introspección vs Encapsulación
La biblioteca
debugrompe la encapsulación por diseño. En lenguajes como Java, necesitas reflection API. En Python, todo es público de todos modos. Lua te da el poder, pero espera que lo uses responsablemente.
Resumen del Capítulo
Introspección en Lua:
debug.getinfo: Información sobre funciones- Stack traces:
debug.getinfo(level) - Variables locales:
debug.getlocal/debug.setlocal - Upvalues:
debug.getupvalue/debug.setupvalue _ENV: El ambiente global, modificable- Sandboxing: Ejecutar código en ambientes limitados
- Profiling y debugging: Herramientas custom con hooks
Próximo: Capítulo 10: Metatablas - El Protocolo de Objetos
Ejercicios
- Stack Trace Mejorado: Incluir valores de variables locales.
function enhanced_stack_trace()
-- Tu código aquí
end
- Sandbox con Límite de Tiempo: Ejecutar código con timeout.
function run_with_timeout(code, timeout_seconds)
-- Tu código aquí (pista: usa debug.sethook con contador)
end
- Inspector de Closures: Mostrar todos los upvalues de una función.
function inspect_closure(fn)
-- Tu código aquí
end
-- >>> local function factory(x, y)
-- >>> local z = x + y
-- >>> return function() return x, y, z end
-- >>> end
-- >>> inspect_closure(factory(1, 2))
-- Upvalues:
-- x = 1
-- y = 2
-- z = 3
Lecturas Adicionales
- Programming in Lua, 4th edition - Capítulo 25: The Debug Library
- Lua 5.4 Reference Manual - Section 6.10: The Debug Library
- Sandboxing in Lua