上周我提交了一个PR,代码大概是这样的:
# config.py
DEBUG = True
API_URL = "https://api.example.com"
TIMEOUT = 30
MAX_RETRIES = 3
# main.py
import config
def fetch_data():
url = config.API_URL
timeout = config.TIMEOUT
# ... 发送请求
if config.DEBUG:
print(f"请求URL: {url}")
def process_result(data):
for attempt in range(config.MAX_RETRIES):
# ... 重试逻辑
if config.DEBUG:
print(f"重试第{attempt+1}次")我用了好几个全局变量(准确说是模块级变量)。代码审查时,同事发来一句话:
"全局变量不是洪水猛兽,但你也用得太多了一点。"
我愣住了。全局变量到底该不该用?什么时候用是合理的?什么时候是坏味道?
这个问题没有标准答案,但有一些准则。今天我把这些准则整理出来,帮你拿捏好"用"和"不用"之间的尺度。
在Python里,"全局变量"这个词被用得很宽泛。实际上有三种不同的"全局":
1. 真正的全局变量(模块级变量)
写在.py文件顶层的变量,整个模块都能访问。
# settings.py
APP_NAME = "我的应用" # 这就是模块级变量
VERSION = "1.0"2. 函数的全局变量(用global声明的)
在函数内部通过global关键字修改的变量。
counter = 0
def increment():
global counter # 让函数可以修改模块级变量
counter += 13. 跨模块共享的变量
通过import在多个模块之间共享的变量。
# state.py
user_count = 0
# module_a.py
import state
state.user_count += 1
# module_b.py
import state
print(state.user_count) # 看到module_a的修改我们讨论的"全局变量",通常指的就是这三种情况。它们的共同点是:生命周期贯穿整个程序运行期间,多个函数或模块都能访问。
在说缺点之前,先承认一个事实:全局变量不是原罪。有些场景用它,代码反而更清晰。
优点1:配置信息天然适合用全局变量
程序的配置参数(API地址、超时时间、调试开关)在运行期间基本不变,而且很多地方都需要用到。把它们放在模块顶层,所有函数都能方便地访问。
# 这些放在模块顶层,清晰且方便
DATABASE_URL = "postgresql://localhost:5432/mydb"
LOG_LEVEL = "INFO"
ENABLE_CACHE = True如果用对象来封装,反而会增加不必要的复杂度。
优点2:缓存和单例状态需要持久化
有些数据需要在多次调用之间保持,比如缓存字典、连接池、全局计数器。
# 缓存结果,避免重复计算
_cache = {}
def get_user(user_id):
if user_id not in _cache:
_cache[user_id] = fetch_from_db(user_id)
return _cache[user_id]这种"全局缓存"是合理的——它确实需要在全局范围内存在。
优点3:减少参数传递的噪音
如果一个变量被很多层函数调用传递,而且每个函数都只是"路过"它,那么用全局变量可以减少参数噪音。
# 不用全局:每一层都要传递debug
def level3(debug):
if debug: print("level3")
def level2(debug):
level3(debug)
def level1(debug):
level2(debug)
# 用全局:只在一处设置,到处可用
DEBUG = True
def level3():
if DEBUG: print("level3")当然,这种便利性是有代价的(后面会说)。
缺点1:隐式依赖,降低可读性
看这个函数:
def calculate_discount(price):
if DISCOUNT_RATE > 0.5: # DISCOUNT_RATE从哪来的?
return price * 0.8
return price * 0.9读代码的人得去模块顶部找DISCOUNT_RATE在哪里定义的。如果这个文件有200行,这增加了认知负担。
相比之下,参数传递是显式的:
def calculate_discount(price, discount_rate):
if discount_rate > 0.5:
return price * 0.8
return price * 0.9一眼就知道这个函数依赖了哪些输入。
缺点2:全局状态让测试变困难
写单元测试时,全局变量是噩梦。因为测试之间会互相影响。
counter = 0
def increment():
global counter
counter += 1
return counter
# test1
assert increment() == 1
# test2
assert increment() == 1 # 会失败!因为counter现在是2要让测试独立,你不得不在每个测试前重置全局变量。这很麻烦,也容易遗漏。
缺点3:并发问题
如果你的程序用了多线程,全局变量需要加锁保护,否则会出现数据竞争。
counter = 0
def increment():
global counter
counter += 1 # 这不是原子操作!多线程下会出问题多个线程同时执行这行代码,可能得到错误的结果。需要用threading.Lock来保护。
缺点4:代码耦合,难以重构
全局变量像一条无形的线,把很多函数串在一起。当你想要修改它的行为时,需要检查所有用到它的地方。
比如把DEBUG从布尔值改成字符串级别("DEBUG"、"INFO"、"WARNING"),所有用到它的函数都得改。
不要简单地"禁止全局变量",而是根据场景判断。我整理了一个决策树:
场景1:配置类变量 → ✅ 可以放心用
程序运行期间基本不变,多个地方需要访问。
# 合理
API_BASE_URL = "https://api.example.com/v1"
MAX_CONNECTIONS = 10
DEFAULT_TIMEOUT = 30场景2:缓存和共享状态 → ⚠️ 谨慎使用
用之前考虑:有没有更好的方式?
# 合理:缓存查询结果
_user_cache = {}
def get_user(id):
if id not in _user_cache:
_user_cache[id] = db.query(id)
return _user_cache[id]
# 但如果是复杂的状态管理,考虑用类封装场景3:函数间传递的临时状态 → ❌ 用参数代替
如果某个值只是在几个函数之间传递,应该用参数,不要用全局变量。
# 不推荐
current_user = None
def set_user(user):
global current_user
current_user = user
def get_user():
return current_user
# 推荐
def process_user(user):
# 直接传递user参数
validate(user)
save(user)场景4:常量值 → ✅ 可以放心用
数学常量、枚举值、固定字符串。
# 合理
PI = 3.1415926
MAX_UPLOAD_SIZE = 10 * 1024 * 1024 # 10MB
DEFAULT_PAGE_SIZE = 20按惯例,常量用全大写命名。
如果你决定减少全局变量,有几种替代方案:
替代1:封装成类
把相关的状态和行为放在一个类里。
# 全局变量方式
counter = 0
def increment():
global counter
counter += 1
# 类封装方式
class Counter:
def __init__(self):
self.count = 0
def increment(self):
self.count += 1
# 使用
c = Counter()
c.increment()当状态和行为绑定在一起时,类比全局变量更清晰。
替代2:用函数参数传递
# 不推荐
DEBUG = True
def log(message):
if DEBUG:
print(f"[DEBUG] {message}")
# 推荐
def log(message, debug=False):
if debug:
print(f"[DEBUG] {message}")
# 调用时显式传递
log("启动完成", debug=True)替代3:用配置对象
如果有大量配置参数,用一个配置对象来集中管理。
# 不推荐:散落的全局变量
DB_HOST = "localhost"
DB_PORT = 5432
DB_NAME = "mydb"
DB_USER = "admin"
DB_PASS = "secret"
def connect():
return connect_to_db(DB_HOST, DB_PORT, DB_NAME, DB_USER, DB_PASS)
# 推荐:配置对象
class Config:
def __init__(self):
self.db_host = "localhost"
self.db_port = 5432
self.db_name = "mydb"
self.db_user = "admin"
self.db_pass = "secret"
config = Config()
def connect():
return connect_to_db(
config.db_host,
config.db_port,
config.db_name,
config.db_user,
config.db_pass
)如果需要,还可以从配置文件或环境变量加载配置。
替代4:用模块级别的单例
Python的模块本身就是单例。如果你需要全局唯一的状态,放在模块里是合理的。
# state.py
_data = {}
def set(key, value):
_data[key] = value
def get(key):
return _data.get(key)
# 其他模块导入state使用这实际上还是用了全局变量,但通过函数接口封装了访问方式,比直接暴露_data要好。
案例1:Web应用的配置
Flask、Django等Web框架都大量使用全局配置。这不是问题,因为配置在运行时基本不变。
# Django的settings.py
SECRET_KEY = "..."
DATABASES = {...}
INSTALLED_APPS = [...]✅ 合理。 配置变量就是全局的,这就是它们的设计目的。
案例2:日志记录器
import logging
logger = logging.getLogger(__name__)
def do_something():
logger.info("doing something")✅ 合理。 日志记录器在整个应用中是唯一的,用全局变量访问非常方便。
案例3:数据库连接池
connection_pool = create_pool()
def get_connection():
return connection_pool.get_connection()⚠️ 需要谨慎。 连接池作为全局单例是常见的,但考虑一下:如果将来需要多个数据库连接池怎么办?用类封装会更灵活。
案例4:用户会话信息
current_user = None
def set_user(user):
global current_user
current_user = user
def show_profile():
print(current_user.name)❌ 不推荐。 用户信息是请求级别的状态,不应该用全局变量存储。Web框架通常用request对象来携带用户信息。
在决定是否使用全局变量前,问自己三个问题:
问题1:这个变量在程序运行期间会变化吗?
问题2:有多少个地方会访问这个变量?
问题3:这个变量的用途是什么?
如果一段代码中出现了以下信号,说明全局变量可能用得太多了:
global声明global声明setUp和tearDown重置一堆全局变量当你看到这些信号时,考虑重构。
使用场景 | 是否推荐 | 原因 | 推荐做法 |
|---|---|---|---|
常量(PI、MAX_SIZE) | ✅ 推荐 | 不变,到处用 | 全大写命名 |
配置(API_URL、DEBUG) | ✅ 推荐 | 基本不变,集中管理 | 放在settings模块 |
缓存(查询结果) | ⚠️ 谨慎 | 有用但容易失控 | 用类封装,提供清理方法 |
单例服务(logger) | ✅ 推荐 | 全局唯一,方便访问 | 模块级变量 |
跨函数传递的临时状态 | ❌ 避免 | 增加隐式依赖 | 用参数传递 |
多线程共享状态 | ⚠️ 谨慎 | 需要锁保护 | 用线程安全的数据结构 |
用户会话信息 | ❌ 避免 | 请求级别,不是全局级别 | 用上下文变量或request对象 |
我那个PR被同事喷了之后,我做了这样几件事:
DEBUG、API_URL、TIMEOUT是配置,保留在模块顶层MAX_RETRIES其实在不同的场景下不同,改成了参数传递改完之后,代码变长了,但变清晰了。审查通过了。
全局变量不是不能用,而是要知道什么时候用、什么时候不该用。
一个简单的判断标准:如果这个变量是"属性"(属于程序本身的配置或常量),可以用全局;如果这个变量是"状态"(描述程序运行过程中的变化),尽量别用全局。
换句话说:配置用全局,状态用对象。
记住这句话,下次写全局变量的时候,你会多思考三秒钟。这三秒钟,可能就是好代码和坏代码的区别。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。