0.全局变量与global关键字基本概念

在编写脚本或工具时,会遇到对于全局变量的引用与赋值操作,一般而言,同一模块内,访问全局变量的方式如下:

# demo.py

var_a = 'global'
def func():
    print(f'Inside: {var_a}')
func()
print(f'Outside: {var_a}')

此时执行打印为:

Inside: global
Outside: global

正如预期的那样,函数func内部正常访问获取了变量var_a的值并进行了打印。接下来需要在函数内部全局修改(更新)变量var_a的值,并编写如下代码:

# demo.py

var_a = 'global'
def func():
    var_a = 'local'
    print(f'Inside: {var_a}')
func()
print(f'Outside: {var_a}')

如上,在func中对var_a进行了赋值,希望var_a的值进行全局修改。运行上述代码后得到结果如下:

Inside: local
Outside: global

这与预期大相径庭,但是明明在函数内部已经能够正常访问外部变量,为什么修改时就会出问题呢?

先将这个问题搁置,引入global关键字并进一步修改代码,如下:

# demo.py

var_a = 'global'
def func():
    global var_a
    var_a = 'local'
    print(f'Inside: {var_a}')
func()
print(f'Outside: {var_a}')

试着运行一下,结果如下:

Inside: local
Outside: local

这与我们的需求一致,但为什么使用global关键字声明后函数内部就能正常修改外部变量呢?

原因在于:

  • 当访问外部变量时,由于这个动作不会修改变量的状态(值),且函数func内部没有对该变量进行显式声明,所以Python解释器会在全局变量内进行查找,当存在同名的全局变量时,解释器缺省认为此时需要访问的是这个全局变量。
  • 当修改外部变量时,由于赋值这个动作对于变量是一种破坏性行为(已有值被覆写),在没有使用global关键字声明引用外部变量的情况下,Python解释器为安全起见会将其理解为局部变量赋值,并创建函数内局部变量。当函数执行完毕时,局部变量销毁,不对同名全局变量产生影响。

在使用global关键字后,显式的声明了func内部使用的变量为全局变量,可以通过查看变量内存地址验证:

# demo.py

var_a = 'global'
print(id(var_a))

def func():
    global var_a
    print(id(var_a))
    var_a = 'local'
    print(f'Inside: {var_a}')
func()

打印可得:

2240955470064
2240955470064
Inside: local

此时,函数内外var_a变脸内存指向一致,成功在函数内作用域访问修改外部全局变量。

1.问题引出

在编写项目级工程时遇到对于全局变量的操作,可以如上文所讲使用global关键字实现。但是这时需求有了一些变化:在不同模块之间实现全局变量的修改,并在引用该变量的其他模块中可以同步变更

先使用global关键字试试:

# a.py

# 定义全局变量
var_a = 'global'

def show():
    print(var_a)
# b.py
from a import var_a, show        # 未使用的import语句 ‘var_a’

def update():
    global var_a        # 全局变量 ‘var_a’在模块级别未定义
    var_a = 'local'

update()
show()

执行时返回:

global

结果预期不符,并且pycahrm会很好心地给我们提示:

  • 未使用的import语句 ‘var_a’
  • 全局变量 ‘var_a’在模块级别未定义

这里主要看第二条警告,说明global关键字仅能在同一模块内声明使用,无法持支跨模块的情况。
那么,有什么方法可以实现这个需求吗?

2.中间文件存储

顾名思义,该方法通过使用一个文件单独将需要的全局变量进行存储,如下图所示:

  • 通过global_var.py文件预定义全局变量var_a,并赋初值。
  • 在文件b.py中引入global_var.py模块,编写方法show()打印var_a
  • 在文件a.py中引入global_var.py模块,编写方法update()修改var_a,并引入show()方法进行打印

代码实现如下:

# global_var.py

# 定义全局变量
var_a = 'global'
# b.py

import global_var

def show():
    print(global_var.var_a)
# a.py

import global_var
from b import show


def update():
    global_var.var_a = 'local'

update()
show()

运行a.py,执行结果如下:

local

优点:

  • 结构简洁且实现容易
  • 在目录层级浅且引用关系不复杂的情况下拥有良好的可维护性

缺点:

  • 仅能对全局变量模块进行全量引入,无法使用from global_var import var_a的方式单独引用某一变量

3.全局变量管理器

该方案与上一种相比稍为复杂,适用于模块在pip包以及其他更为复杂的情况。

该方案利用sys.modules['__main__'].__dict__与主文件调用globals()方法指向一致的原理,直接修改当前作用域的全局变量字典,实现全局变量管理器如下:

# global_manager.py

import sys
from typing import Any


class GlobalManager:
    @staticmethod
    def set_global_dct(key: str, value: Any):
        glb_dct = sys.modules['__main__'].__dict__
        glb_dct[key] = value

    @staticmethod
    def get_global_dct(key: str):
        return sys.modules['__main__'].__dict__[key]

    @staticmethod
    def get_global_list():
        return sys.modules['__main__'].__dict__.keys()

这时,修改代码如下:

# b.py

from global_manager import GlobalManager

GlobalManager.set_global_dct('var_a', 'global')

def show():
    print(GlobalManager.get_global_dct('var_a'))
# a.py

from global_manager import GlobalManager
from b import show

def update():
    GlobalManager.set_global_dct('var_a', 'local')

update()
show()

运行a.py,执行结果如下:

local

该方案图示如下:

优点:

  • 可实现按需调用全局变量
  • 不依赖单独文件定义全局变量,可在任意文件中定义。
  • 可拓展性较强

缺点:

  • 变量调用语法较为复杂,实际使用时需进行二次封装