首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >写Python函数,什么时候该用全局变量?

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

原创
作者头像
风一样的男子
发布2026-06-22 13:40:17
发布2026-06-22 13:40:17
840
举报
文章被收录于专栏:编程教程编程教程

一个让我被同事喷了一顿的PR

上周我提交了一个PR,代码大概是这样的:

代码语言:javascript
复制
# 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文件顶层的变量,整个模块都能访问。

代码语言:javascript
复制
# settings.py
APP_NAME = "我的应用"   # 这就是模块级变量
VERSION = "1.0"

2. 函数的全局变量(用global声明的)

在函数内部通过global关键字修改的变量。

代码语言:javascript
复制
counter = 0

def increment():
    global counter   # 让函数可以修改模块级变量
    counter += 1

3. 跨模块共享的变量

通过import在多个模块之间共享的变量。

代码语言:javascript
复制
# 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地址、超时时间、调试开关)在运行期间基本不变,而且很多地方都需要用到。把它们放在模块顶层,所有函数都能方便地访问。

代码语言:javascript
复制
# 这些放在模块顶层,清晰且方便
DATABASE_URL = "postgresql://localhost:5432/mydb"
LOG_LEVEL = "INFO"
ENABLE_CACHE = True

如果用对象来封装,反而会增加不必要的复杂度。

优点2:缓存和单例状态需要持久化

有些数据需要在多次调用之间保持,比如缓存字典、连接池、全局计数器。

代码语言:javascript
复制
# 缓存结果,避免重复计算
_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:减少参数传递的噪音

如果一个变量被很多层函数调用传递,而且每个函数都只是"路过"它,那么用全局变量可以减少参数噪音。

代码语言:javascript
复制
# 不用全局:每一层都要传递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:隐式依赖,降低可读性

看这个函数:

代码语言:javascript
复制
def calculate_discount(price):
    if DISCOUNT_RATE > 0.5:   # DISCOUNT_RATE从哪来的?
        return price * 0.8
    return price * 0.9

读代码的人得去模块顶部找DISCOUNT_RATE在哪里定义的。如果这个文件有200行,这增加了认知负担。

相比之下,参数传递是显式的:

代码语言:javascript
复制
def calculate_discount(price, discount_rate):
    if discount_rate > 0.5:
        return price * 0.8
    return price * 0.9

一眼就知道这个函数依赖了哪些输入。

缺点2:全局状态让测试变困难

写单元测试时,全局变量是噩梦。因为测试之间会互相影响。

代码语言:javascript
复制
counter = 0

def increment():
    global counter
    counter += 1
    return counter

# test1
assert increment() == 1

# test2
assert increment() == 1  # 会失败!因为counter现在是2

要让测试独立,你不得不在每个测试前重置全局变量。这很麻烦,也容易遗漏。

缺点3:并发问题

如果你的程序用了多线程,全局变量需要加锁保护,否则会出现数据竞争。

代码语言:javascript
复制
counter = 0

def increment():
    global counter
    counter += 1  # 这不是原子操作!多线程下会出问题

多个线程同时执行这行代码,可能得到错误的结果。需要用threading.Lock来保护。

缺点4:代码耦合,难以重构

全局变量像一条无形的线,把很多函数串在一起。当你想要修改它的行为时,需要检查所有用到它的地方。

比如把DEBUG从布尔值改成字符串级别("DEBUG"、"INFO"、"WARNING"),所有用到它的函数都得改。


第四步:什么时候该用?——一个决策框架

不要简单地"禁止全局变量",而是根据场景判断。我整理了一个决策树:

场景1:配置类变量 → ✅ 可以放心用

程序运行期间基本不变,多个地方需要访问。

代码语言:javascript
复制
# 合理
API_BASE_URL = "https://api.example.com/v1"
MAX_CONNECTIONS = 10
DEFAULT_TIMEOUT = 30

场景2:缓存和共享状态 → ⚠️ 谨慎使用

用之前考虑:有没有更好的方式?

代码语言:javascript
复制
# 合理:缓存查询结果
_user_cache = {}
def get_user(id):
    if id not in _user_cache:
        _user_cache[id] = db.query(id)
    return _user_cache[id]

# 但如果是复杂的状态管理,考虑用类封装

场景3:函数间传递的临时状态 → ❌ 用参数代替

如果某个值只是在几个函数之间传递,应该用参数,不要用全局变量。

代码语言:javascript
复制
# 不推荐
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:常量值 → ✅ 可以放心用

数学常量、枚举值、固定字符串。

代码语言:javascript
复制
# 合理
PI = 3.1415926
MAX_UPLOAD_SIZE = 10 * 1024 * 1024  # 10MB
DEFAULT_PAGE_SIZE = 20

按惯例,常量用全大写命名。


第五步:不用全局变量,用什么呢?

如果你决定减少全局变量,有几种替代方案:

替代1:封装成类

把相关的状态和行为放在一个类里。

代码语言:javascript
复制
# 全局变量方式
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:用函数参数传递

代码语言:javascript
复制
# 不推荐
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:用配置对象

如果有大量配置参数,用一个配置对象来集中管理。

代码语言:javascript
复制
# 不推荐:散落的全局变量
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的模块本身就是单例。如果你需要全局唯一的状态,放在模块里是合理的。

代码语言:javascript
复制
# state.py
_data = {}

def set(key, value):
    _data[key] = value

def get(key):
    return _data.get(key)

# 其他模块导入state使用

这实际上还是用了全局变量,但通过函数接口封装了访问方式,比直接暴露_data要好。


第六步:几个真实案例

案例1:Web应用的配置

Flask、Django等Web框架都大量使用全局配置。这不是问题,因为配置在运行时基本不变。

代码语言:javascript
复制
# Django的settings.py
SECRET_KEY = "..." 
DATABASES = {...}
INSTALLED_APPS = [...]

合理。 配置变量就是全局的,这就是它们的设计目的。

案例2:日志记录器

代码语言:javascript
复制
import logging
logger = logging.getLogger(__name__)

def do_something():
    logger.info("doing something")

合理。 日志记录器在整个应用中是唯一的,用全局变量访问非常方便。

案例3:数据库连接池

代码语言:javascript
复制
connection_pool = create_pool()

def get_connection():
    return connection_pool.get_connection()

⚠️ 需要谨慎。 连接池作为全局单例是常见的,但考虑一下:如果将来需要多个数据库连接池怎么办?用类封装会更灵活。

案例4:用户会话信息

代码语言:javascript
复制
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. 移除了不必要的全局:有些函数里用全局变量只为了少传一个参数,我都改成了显式传递

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


最后一句总结

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

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

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

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

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一个让我被同事喷了一顿的PR
  • 第一步:先搞清楚我们说的是哪种"全局变量"
  • 第二步:全局变量的优点——它确实有用
  • 第三步:全局变量的缺点——它确实有风险
  • 第四步:什么时候该用?——一个决策框架
  • 第五步:不用全局变量,用什么呢?
  • 第六步:几个真实案例
  • 第七步:一个参考标准——"全局变量的3个问题"
  • 第八步:代码中的"信号"
  • 一张决策表
  • 回到开头的那个PR
  • 最后一句总结
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档