最近在看一段代码时,发现作者使用globals()来应对无法确定变量名的情况,故此对该函数进行了详细的了解。
一、基本情况
globals() 是Python的标准内置函数,用于返回当前模块中定义的全局变量的字典(包含所有的全局变量、函数、类、导入的模块) ,其中键是变量名,值是变量的值。字典可进行修改,修改后会随之反映到全局变量上。对于函数内的代码,返回的为在定义函数时确定的该字典,无论在哪里调用都保持不变。
globals是特殊场景的解决方案,如动态批量处理全局变量/函数(eg:配置加载)、调试时快速排查全局状态问题、框架/库的底层实现,且有完善的封装和文档。
缺点:可读性差,命名冲突、作用域误区、线程不安全、性能损耗、破坏封装
重点:非必要不使用,优先使用字典/类进行替代,使用时做好校验、锁和注释。
二、常用场景实例
核心价值是动态操作全局命名空间 ,由于操作的是全局状态,使用时需要谨慎。
1、批量处理配置变量、根据配置变量名读取对应的值
python
config0= 10
config1 = 30
config2 = "INFO"
# 变量名来自外部(如配置文件、用户输入)
config_keys = ["config0", "config1", "config2"]
# 动态读取所有配置
for key in config_keys:
if key in globals():
print(f"{key}: {globals()[key]}")
运行结果:

2、批量初始化
编码前,无法预知所有的变量名或需要生成大量相似的变量名,需要通过代码逻辑批量生成全局变量

3、函数内部修改全局变量
编码需要时,则需要使用,此处不进行举例说明了。
三、使用注意事项
globals()直接操作全局命名空间,滥用会导致代码可读性、可维护性大幅下降,甚至引发隐藏bug
1、严重降低代码可读性与可维护性
动态操作的变量/函数名是字符串,IDE无法自动补全、跳转,代码审查时难以追踪变量的定义/修改位置,后期维护成本高,如下图所示:

上方globals()里面变量/函数内容很多,某个变量随时可能被其他方法更改,当出现问题时,排查很困难。下方单独使用var_dict很清晰能知道变量是在哪里来的,在哪里修改的。
2、易引发命名冲突与意外覆盖
全局命名空间包含模块顶层定义的变量、函数、类等,globals()动态创建/修改变量,若名称与已有重复,则会静默覆盖原有对象。
在程序中,如不小心覆盖内置函数,在访问中可能会遇到报错,如下所示

在程序中,如多处动态给某个变量赋值,则会造成功能错误

如上所示,本身天津和重庆都有叫张三的,结果由于程序后面进行了覆盖,只保留了最后的值。这种在程序中也不存在语法错误,在排查bug时会比较麻烦
建议:动态命名时加前缀,避免与内置名、已有变量重名
操作赋值前,先校验是否为已有变量
3、globals()作用区域理解存在误区
python中的几种作用域可详见:https://blog.csdn.net/qq_44801116/article/details/158041678?spm=1001.2014.3001.5501
很多人会误以为globals()可以获取到外层函数作用域,实际是未搞清楚外层函数的变量的作用域为嵌套作用域,不会影响全局变量。嵌套函数想访问外层函数变量,需要使用nonlocal或local(),而非globals()

4、多线程时,存在线程安全问题
多线程同时通过globals()读写全局变量时,同时修改一个变量,会导致数据不一致。
python
count = 0
def increment():
for _ in range(100000):
globals()["count"] += 1
t1 = threading.Thread(target=increment)
t2 = threading.Thread(target=increment)
t1.start()
t2.start()
t1.join()
t2.join()
print(count)
运行结果是200000,但是实际结果是随机,如下

可以通过加锁的形式,访问全局变量从而保证结果的正确性。
python
def increment():
for _ in range(100000):
while threading.Lock():
globals()["count"] += 1
5、性能损耗:尤其频繁操作
globals返回的字典,字典的键查找/赋值比直接访问变量要慢,尤其在频繁操作时会累积性能损耗。
建议循环中需要频繁操作局部变量时,先将赋值给局部变量,操作完成后再赋值给全局变量