免费编程软件「python+pycharm」
链接:https://pan.quark.cn/s/48a86be2fdc0
一个让我被同事喷了一顿的PR
上周我提交了一个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对象来携带用户信息。
第七步:一个参考标准——"全局变量的3个问题"
在决定是否使用全局变量前,问自己三个问题:
问题1:这个变量在程序运行期间会变化吗?
不会(常量)→ ✅ 放心用
会,但变化极少(配置)→ ✅ 可以接受
会频繁变化(状态)→ ⚠️ 考虑用类封装
问题2:有多少个地方会访问这个变量?
1-2个函数 → 考虑用参数传递
3-5个函数 → 可以考虑用全局
5个以上 → 考虑用配置对象或类
问题3:这个变量的用途是什么?
配置/常量 → ✅ 适合全局
缓存 → ⚠️ 谨慎,确保有清理策略
临时状态 → ❌ 避免全局
单例服务 → ⚠️ 考虑用依赖注入
第八步:代码中的"信号"
如果一段代码中出现了以下信号,说明全局变量可能用得太多了:
一个模块里有超过5个
global声明函数里有超过3个
global声明全局变量被修改的地方超过10处
测试文件里需要
setUp和tearDown重置一堆全局变量你无法在同一个进程里运行两次不同的测试(状态冲突)
当你看到这些信号时,考虑重构。
一张决策表
| 使用场景 | 是否推荐 | 原因 | 推荐做法 |
|---|---|---|---|
| 常量(PI、MAX_SIZE) | ✅ 推荐 | 不变,到处用 | 全大写命名 |
| 配置(API_URL、DEBUG) | ✅ 推荐 | 基本不变,集中管理 | 放在settings模块 |
| 缓存(查询结果) | ⚠️ 谨慎 | 有用但容易失控 | 用类封装,提供清理方法 |
| 单例服务(logger) | ✅ 推荐 | 全局唯一,方便访问 | 模块级变量 |
| 跨函数传递的临时状态 | ❌ 避免 | 增加隐式依赖 | 用参数传递 |
| 多线程共享状态 | ⚠️ 谨慎 | 需要锁保护 | 用线程安全的数据结构 |
| 用户会话信息 | ❌ 避免 | 请求级别,不是全局级别 | 用上下文变量或request对象 |
回到开头的那个PR
我那个PR被同事喷了之后,我做了这样几件事:
区分了配置和状态:
DEBUG、API_URL、TIMEOUT是配置,保留在模块顶层封装了需要变化的状态:
MAX_RETRIES其实在不同的场景下不同,改成了参数传递移除了不必要的全局:有些函数里用全局变量只为了少传一个参数,我都改成了显式传递
改完之后,代码变长了,但变清晰了。审查通过了。
最后一句总结
全局变量不是不能用,而是要知道什么时候用、什么时候不该用。
一个简单的判断标准:如果这个变量是"属性"(属于程序本身的配置或常量),可以用全局;如果这个变量是"状态"(描述程序运行过程中的变化),尽量别用全局。
换句话说:配置用全局,状态用对象。
记住这句话,下次写全局变量的时候,你会多思考三秒钟。这三秒钟,可能就是好代码和坏代码的区别。