Python 作用域与命名空间:全局/局部变量及 LEGB 规则解析
背景:全局变量带来的安全隐患
来看一段代码示例:
# config.py
DB_HOST = "192.168.1.10"
DB_PORT = 54321
DB_USER = "app_user"
DB_PASSWORD = "secret123"
def connect_to_db():
return psycopg2.connect(
host=DB_HOST,
port=DB_PORT,
user=DB_USER,
password=DB_PASSWORD
)
潜在风险:
- 密码暴露在全局命名空间,任何模块
import config都能读取 - 多线程环境下,若有人临时修改了
DB_PASSWORD,整个进程可能受影响 - 单元测试时,难以 Mock 不同环境的配置
这背后,就是 作用域(Scope)和命名空间(Namespace) 的概念需要理清。
一、LEGB 是什么?
LEGB 是 Python 查找变量的顺序:
- Local(局部) → 函数内部
- Enclosing(嵌套) → 外层函数(闭包)
- Global(全局) → 模块顶层
- Built-in(内置) →
len,print等
示例:
name = "全局张三" # Global
def outer():
name = "外层李四" # Enclosing
def inner():
name = "局部王五" # Local
print(name)
inner()
outer()
Python 会从内往外找:先看 inner() 里有没有 name,有就用;没有再看 outer(),再没有才看全局。
核心原则:'就近原则,层层向外'
二、全局变量:合理使用需谨慎
很多开发者习惯把配置全扔全局:
# ❌ 潜在风险示范
DB_CONFIG = {
"host": "prod-db",
"port": 54321,
"user": "admin",
"password": "生产密码"
}
def get_connection():
return psycopg2.connect(**DB_CONFIG)
问题在哪?
- 安全风险:
DB_CONFIG可被任意代码修改或打印 - 测试困难:无法为测试换一套配置
- 隐式依赖:函数行为依赖外部状态,违反'纯函数'原则
✅ 建议方案:用函数封装 + 局部作用域
def get_db_config(env="prod"):
"""根据环境返回配置(局部变量,用完即焚)"""
configs = {
"dev": {"host": "localhost", "port": 54321, "user": "dev", "password": "dev123"},
"prod": {"host": "db-cluster", "port": 54321, "user": "app", "password": _get_prod_password()}
}
return configs[env]
def _get_prod_password():
"""从安全存储读取(如环境变量),不在代码中硬编码"""
import os
return os.getenv("DB_PROD_PASS")
def create_db_connection(env="prod"):
config = get_db_config(env) # config 是局部变量!
return psycopg2.connect(**config)
核心要点:
config只在函数内部存在,外部无法访问,用完自动销毁。
三、global 关键字:能不用就不用
有些人为了'方便',在函数里直接改全局变量:
# ❌ 反模式
connection_pool = None
def init_pool():
global connection_pool # 声明要改全局
connection_pool = create_pool() # 危险!
后果:
- 函数有副作用(修改外部状态)
- 多线程下竞态条件(race condition)
- 单元测试互相污染
✅ 替代方案:返回值 or 类封装
# 方案 1:返回新值
def create_connection_pool():
return ConnectionPool(...)
pool = create_connection_pool() # 调用者自己管理
# 方案 2:用类封装状态(推荐)
class DatabaseClient:
def __init__(self, env="prod"):
self.config = get_db_config(env)
self._pool = None
def get_connection(self):
if self._pool is None:
self._pool = self._create_pool()
return self._pool
四、闭包(Enclosing):高级用法
闭包常用于工厂函数,比如创建带预设参数的查询器:
def make_query_executor(table_name):
"""返回一个专门查某表的函数(闭包)"""
def execute_query(where_clause="", params=None):
sql = f"SELECT * FROM {table_name}"
if where_clause:
sql += f" WHERE {where_clause}"
with get_db_connection() as conn:
with conn.cursor() as cur:
cur.execute(sql, params)
return cur.fetchall()
return execute_query
# 使用
query_students = make_query_executor("students")
data = query_students("grade > %s", (85,))
优点:
- 封装表名,避免重复代码
- 函数携带上下文,调用更简洁
风险:
- 如果
table_name来自用户输入,必须严格校验! - 闭包变量不可变(想改需用
nonlocal,但一般不建议)
五、最佳实践:数据库连接管理
结合 LEGB 原则,我们写一个生产级连接工具:
# db_client.py
import os
import psycopg2
from typing import Optional
# 全局只放'不变'的东西(如驱动名)
DRIVER_NAME = "psycopg2"
def _load_credentials(env: str) -> dict:
"""从环境变量加载凭据(局部作用域,安全)"""
prefix = f"DB_{env.upper()}_"
return {
"host": os.getenv(f"{prefix}HOST"),
"port": int(os.getenv(f"{prefix}PORT", 54321)),
"database": os.getenv(f"{prefix}DB", "default"),
"user": os.getenv(f"{prefix}USER"),
"password": os.getenv(f"{prefix}PASS"),
}
class ConnectionManager:
def __init__(self, env: str = "prod"):
self.env = env
# credentials 是实例属性(局部于对象),非全局
self.credentials = _load_credentials(env)
def get_connection(self):
"""每次返回新连接(避免连接复用问题)"""
if not all(self.credentials.values()):
raise ValueError(f"DB {self.env} 配置不完整")
return psycopg2.connect(**self.credentials)
if __name__ == "__main__":
manager = ConnectionManager("dev")
with manager.get_connection() as conn:
with conn.cursor() as cur:
cur.execute("SELECT version()")
print(cur.fetchone())
设计优势:
- 无全局敏感数据:凭据只在
_load_credentials局部存在 - 环境隔离:不同
ConnectionManager实例互不影响 - 可测试:可通过 mock 环境变量测试不同场景
六、常见陷阱
陷阱一:在循环里定义 lambda,结果全一样
# 错误示范
funcs = []
for i in range(3):
funcs.append(lambda: print(i))
for f in funcs:
f() # 输出:2, 2, 2 ❌
✅ 解法:用默认参数捕获当前值
for i in range(3):
funcs.append(lambda x=i: print(x)) # x=i 在定义时求值
陷阱二:误用 global 导致单元测试失败
counter = 0
def increment():
global counter
counter += 1
# 测试 1
increment() # counter=1
# 测试 2(没重置 counter)→ 结果错误!
✅ 解法:状态交给调用者管理
def increment(counter):
return counter + 1
# 测试
c = 0
c = increment(c) # 清晰可控
七、安全建议
- 永远不要在代码中硬编码密码 用环境变量、Vault 或 KMS,且只在局部作用域使用。
- 连接对象不要全局共享
全局连接池若设计不当,会导致:
- 连接泄漏
- 事务交叉污染
- 线程安全问题
推荐用
with语句或连接池库。
- 了解数据库的企业级能力 数据库支持高可用架构,但你的代码如果因作用域混乱导致连接错乱,再强的数据库也救不了你。
总结
在核心系统里,变量的作用域,就是你的安全边界。
记住三条铁律:
- 敏感数据,绝不全局
- 函数无副作用,状态靠返回
- 能局部,不全局;能参数,不隐式
下次写代码前,问自己:
'这个变量,真的需要让全世界都知道吗?'
如果答案是否定的——
把它关进局部作用域的笼子里,才是对系统最大的负责。

