En Python, al definir una función, el espacio de variables se divide en variables globales y variables locales. Si se define dentro de un método de clase, existe además un espacio para variables miembro (self). Entonces, ¿hay alguna forma de separar estos diferentes espacios de variables en la práctica?
1. Lectura y Modificación de Variables Locales
Primero, veamos cómo leer variables locales. Generalmente existen varios métodos como locals(), vars() y sys._getframe(0).f_code.co_varnames. También existe un método sys._getframe(0).f_locals que es equivalente a locals(). El código de implementación relacionado es el siguiente:
valor = 0
class Objeto:
def __init__(self,parametro):
self.metodo(parametro)
def metodo(parametro, valor_predeterminado=1):
interna = 2
print (locals())
print (vars())
print (__import__('sys')._getframe(0).f_code.co_varnames)
if __name__ == '__main__':
Objeto(2)
El resultado de ejecutar este código es:
{'self': <__main__.Objeto object at 0x7f5cf5e74e50>, 'parametro': 2, 'valor_predeterminado': 1, 'interna': 2}
{'self': <__main__.Objeto object at 0x7f5cf5e74e50>, 'parametro': 2, 'valor_predeterminado': 1, 'interna': 2}
('self', 'parametro', 'valor_predeterminado', 'interna')
Cuando se utiliza el método vars sin especificar un nombre de varible, es equivalente al método locals(). Ambos devuelten resultados en formato diccionario. Si se ejecutan dentro de un método de clase, incluirán una variable del tipo main.Objeto object, que representa todas las variables meimbro de self, siendo en realidad parte de las variables locales.
Al utilizar el método co_varnames, obtenemos los nombres de todas las variables locales. Podemos definir una variable miembro adicional en el ejemplo:
valor = 0
class Objeto:
def __init__(self, parametro):
self.propiedad = 5
self.metodo(parametro)
def metodo(self, parametro, valor_predeterminado=1):
interna = 2
print(locals())
print(vars())
print(__import__('sys')._getframe(0).f_code.co_varnames)
if __name__ == '__main__':
Objeto(2)
Se observa que todas las variables miembro se incluyen en self. Es importante notar que la variable global valor nunca aparece en las variables locales.
Ahora que podemos separar las variables locales o sus nombres, ¿cómo podemos modificar estas variables locales? Primero, debemos entender que el método locals() devuelve una copia de las variables. Esto significa que incluso si modificamos el resultado devuelto por locals(), no podemos cambiar realmente el valor de la variable local original. Para ilustrar esto, veamos el siguiente ejemplo:
valor = 0
class Objeto:
def __init__(self,parametro):
self.metodo(parametro)
def metodo(self, parametro, valor_predeterminado=1):
interna = 2
vars()['valor_predeterminado']=2
locals()['nueva']=3
print (locals())
print (valor_predeterminado)
if __name__ == '__main__':
Objeto(2)
En este ejemplo, intentamos modificar el valor de las variables locales mediante los métodos vars() y locals(). El resultado final es:
{'self': <__main__.Objeto object at 0x7f74d9470e50>, 'parametro': 2, 'valor_predeterminado': 1, 'interna': 2, 'nueva': 3}
1
Primero, explicamos por qué la variable nueva no se imprime. Como mencionamos, el valor devuelto por vars y locals es una copia de las variables reales. Por lo tanto, cualquier modificación o adición no se sincronizará con el espacio de variables. En otras palabras, la variable local nueva sigue sin estar definida, existiendo solo en el diccionario locals o vars. Intentar imprimirla resultaría en un error NameError. El valor final impreso de valor_predeterminado es 1, lo que indica que su valor no se vio afectado por la modificación en el diccionario vars.
Entonces, ¿hay alguna forma de modificar variables locales mediante cadenas de texto (sin afectar a las variables globales)? La respuesta es sí, pero esta solución es bastante poco convencional. Veamos el siguiente ejemplo:
import ctypes
valor = 0
class Objeto:
def __init__(self,parametro):
self.metodo(parametro)
def metodo(self, parametro, valor_predeterminado=1):
interna = 2
__import__('sys')._getframe(0).f_locals.update({
'valor_predeterminado': 2,'nueva': 3
})
ctypes.pythonapi.PyFrame_LocalsToFast(
ctypes.py_object(__import__('sys')._getframe(0)), ctypes.c_int(0))
print (locals())
print (valor_predeterminado)
if __name__ == '__main__':
Objeto(2)
Este ejemplo utiliza Cython para modificar directamente el contenido del marco de datos. El f_locals utilizado es esencialmente locals(). Después de la ejecución, el resultado es:
{'self': <__main__.Objeto object at 0x7fea2e2a1e80>, 'parametro': 2, 'valor_predeterminado': 2, 'interna': 2, 'nueva': 3}
2
En este caso, la variable local valor_predeterminado se modificó exitosamente. Sin embargo, incluso utilizando este método, no podemos crear una nueva varible local. Intentar ejecutar print(nueva) aún resultaría en un error.
2. Lectura y Modificación de Variables Globales
En comparación con la modificación de variables locales, ver y modificar variables globales es más sencillo. Primero, usemos un ejemplo para mostrar cómo ver todas las variables globales:
valor = 0
class Objeto:
def __init__(self,parametro):
self.metodo(parametro)
def metodo(self, parametro, valor_predeterminado=1):
interna = 2
print (globals())
if __name__ == '__main__':
Objeto(2)
Mientras que hay muchos métodos para obtener variables locales, generalmente se utiliza globals o su equivalente f_globals para obtener variables globales. La salida del código anterior es:
{'__name__': '__main__', '__doc__': None, '__package__': None,
'__loader__': <_frozen_importlib_external.SourceFileLoader object at 0x7f202632ac40>,
'__spec__': None, '__annotations__': {}, '__builtins__': <module 'builtins' (built-in)>,
'__file__': 'xxx.py', '__cached__': None, 'valor': 0, 'Objeto': <class '__main__.Objeto'>}
Con este método, encontramos la variable global valor, mientras que las variables locales dentro de la misma función no aparecen en las claves de globals. A diferencia de las variables locales, la función globals devuelve datos reales que se pueden modificar directamente y tendrán efecto globalmente.
Por ejemplo, podemos definir o modificar variables globales dentro de una función:
valor = 0
class Objeto:
def __init__(self,parametro):
self.metodo(parametro)
def metodo(self, parametro, valor_predeterminado=1):
global interna
interna = 2
globals()['valor']=3
if __name__ == '__main__':
Objeto(2)
print(globals()['valor'])
print(globals()['interna'])
En este ejemplo, observamos que no solo el valor modificado de valor tiene efecto, sino que la nueva variable interna también se sincroniza con las variables globales, permitiendo una asignación o modificación uniforme entre variables globales y locales.
3. Lectura y Modificación de Variables Miembro
En Python, cada objeto definido tiene un atributo oculto dict, que es un diccionario que contiene todos los nombres y valores de las variables miembro. En un artículo anterior, explicamos cómo utilizar dict para asignar valores a variables miembro de una clase, lo cual resulta muy conveniente. Veamos un ejemplo para ver el contenido de dict:
valor = 0
class Objeto:
def __init__(self,parametro):
self.interna = 2
self.metodo(parametro)
def metodo(self, parametro, valor_predeterminado=1):
print (self.__dict__)
if __name__ == '__main__':
Objeto(2)
Del resultado de salida, podemos ver que dict muestra un contenido muy limpio, con todos los nombres y valores de las variables miembro. Aunque las variables miembro son atributos de un objeto, su forma de operación es muy similar a la de las variables globales globals, a diferencia de locals que es de solo lectura. Un ejemplo específico es el siguiente:
valor = 0
class Objeto:
def __init__(self,parametro):
self.interna = 2
self.metodo(parametro)
def metodo(self, parametro, valor_predeterminado=1):
self.interna = 5
self.__dict__['nueva'] = 6
print (self.__dict__)
print (self.interna, self.nueva)
if __name__ == '__main__':
Objeto(2)
En este ejemplo, modificamos el valor de una variable miembro y también creamos una nueva variable miembro utilizando dict. Podemos ver que ambos cambios se sincronizaron con el espacio de variables, completando así la modificación de variables miembro.
4. Conclusión
Python es un lenguaje de programación bastante flexible y conveniente, pero esta comodidad a veces conlleva ciertos riesgos, como la implementación de funciones integradas como exec y eval que pueden causar problemas de escape de sandbox. Sin embargo, a veces necesitamos operaciones por lotes, como crear o modificar variables locales, globales o miembro de manera masiva, lo que requiere que primero guardemos todos los nombres de variables como cadenas y luego los usemos como nombres de variables cuando sea necesario.
En este artículo, hemos presentado una serie de operaciones que no utilizan exec y eval (aunque no están exentas de riesgos, ya que hemos utilizado ctypes y marcos de datos definidos en sys) para ver, definir y modificar los diversos tipos de variables necesarios.