当前位置: 首页 > news >正文

写Python函数,什么时候该用全局变量?

免费编程软件「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 += 1

3. 跨模块共享的变量

通过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处

  • 测试文件里需要setUptearDown重置一堆全局变量

  • 你无法在同一个进程里运行两次不同的测试(状态冲突)

当你看到这些信号时,考虑重构。


一张决策表

使用场景是否推荐原因推荐做法
常量(PI、MAX_SIZE)✅ 推荐不变,到处用全大写命名
配置(API_URL、DEBUG)✅ 推荐基本不变,集中管理放在settings模块
缓存(查询结果)⚠️ 谨慎有用但容易失控用类封装,提供清理方法
单例服务(logger)✅ 推荐全局唯一,方便访问模块级变量
跨函数传递的临时状态❌ 避免增加隐式依赖用参数传递
多线程共享状态⚠️ 谨慎需要锁保护用线程安全的数据结构
用户会话信息❌ 避免请求级别,不是全局级别用上下文变量或request对象

回到开头的那个PR

我那个PR被同事喷了之后,我做了这样几件事:

  1. 区分了配置和状态DEBUGAPI_URLTIMEOUT是配置,保留在模块顶层

  2. 封装了需要变化的状态MAX_RETRIES其实在不同的场景下不同,改成了参数传递

  3. 移除了不必要的全局:有些函数里用全局变量只为了少传一个参数,我都改成了显式传递

改完之后,代码变长了,但变清晰了。审查通过了。


最后一句总结

全局变量不是不能用,而是要知道什么时候用什么时候不该用

一个简单的判断标准:如果这个变量是"属性"(属于程序本身的配置或常量),可以用全局;如果这个变量是"状态"(描述程序运行过程中的变化),尽量别用全局。

换句话说:配置用全局,状态用对象

记住这句话,下次写全局变量的时候,你会多思考三秒钟。这三秒钟,可能就是好代码和坏代码的区别。

http://www.jsqmd.com/news/1065090/

相关文章:

  • keytool-importkeypair:3分钟搞定Java密钥库导入难题的终极方案
  • Claude Skills 入门:结构化能力模块的定义与实战构建
  • 轻松下载全网视频:Video-Downloader完整使用指南
  • Kubernetes第五天学习指南:集群交互与 Namespace
  • Java小白也能学会!收藏这份RAG大模型实战指南,轻松玩转文档问答
  • Dify接入GLM-4.7的协议适配实践
  • 深入解析AMM交易轨道:从恒定乘积到加权乘积的数学原理与应用
  • 基于核方法与模型集成的LLM认知不确定性量化实践
  • 5分钟快速上手:Better BibTeX插件让你的Zotero文献管理效率翻倍
  • 2026年更新:探寻盐城诚信的滑台直销工厂,助力精密制造升级 - 品牌鉴赏官2026
  • (2026最新)大理防水补漏正规公司甄选推荐:漏水检测维修-暗管漏水精准定位检测漏水点-卫生间/厨房/屋顶/阳台/渗漏水维修-本地人必选的正规测漏公司 - 即刻修防水
  • ClaudeCode Agent核心循环:四层防御式执行架构解析
  • 计算机毕业设计之高速公路交通流量预测算法
  • 无名杀:开源三国杀网页版终极体验指南
  • AI时代架构师的重定义:从画图者到系统导演
  • 钢结构易发生的工程事故有哪些?
  • 揭秘 3C 认证背后强制消防指标,采购对标不踩坑
  • (2026最新)唐山防水补漏正规公司甄选推荐:漏水检测维修-暗管漏水精准定位检测漏水点-卫生间/厨房/屋顶/阳台/渗漏水维修-本地人必选的正规测漏公司 - 即刻修防水
  • 5分钟快速掌握:用Mermaid Live Editor让技术图表创作变得如此简单
  • 基于NLP的本地新闻与移民社区需求智能匹配系统设计与实现
  • 2026年度华南地区办公室家具市场趋势分析:五大品牌评测与采购要点
  • 029、自定义命令开发:创建、参数化、共享与团队复用的最佳实践
  • keytool-importkeypair终极指南:如何快速解决Java密钥管理难题
  • 实时音频对话事实核查系统:多模态AI在信息验证中的工程实践
  • 极智词元企业级RAG系统优化实践:从60分到95分的进阶之路
  • 1M上下文实战:JavaAI插件配置、压缩与压测全链路
  • 产业园区精细化运营时代:第三方专业运营服务模式与实践观察
  • 2026年钟楼区渗水维修企业哪家好,窗户漏水维修/阳台漏水维修/墙面渗水维修/屋顶漏水维修,渗水维修企业哪家好 - 品牌推荐师
  • 软考高项论文总卡 45 分?学长拆解阅卷 5 大得分点,照着写不踩坑
  • 2026年重庆机电安装市场新观察:甄选可靠服务商的战略指南 - 品牌鉴赏官2026