小陈那天写了一个电商后台的配置模块,把数据库地址、缓存地址、调试开关都放在一个 config.py 里:
# config.py
DATABASE_URL = "mysql://localhost:3306/shop"
REDIS_URL = "redis://localhost:6379/0"
DEBUG = False
然后在各个业务模块里引用配置:
# order_service.py
from config import DATABASE_URL, DEBUG
def query_order(order_id):
if DEBUG:
print(f"查询订单: {order_id}")
# 使用 DATABASE_URL 连接数据库
项目上线后,运维想在运行时把 DEBUG 打开,排查一个问题。他在启动脚本里加了这样一段:
import config
config.DEBUG = True
然后调用 order_service.query_order(),结果 DEBUG 还是 False,print 根本没执行。运维很困惑:“我明明把 config.DEBUG 改成 True 了,为什么 order_service 里还是 False?”
小陈也纳闷,去查了 order_service.py 的代码,确认写的是 from config import DEBUG。他试着在 order_service 里打印 config.DEBUG:
import config
print(config.DEBUG) # True
print(DEBUG) # False
同一个变量,两种方式拿到不同的值。小陈愣住了:“config.DEBUG 是 True,DEBUG 怎么还是 False?它们不是同一个东西吗?”
不是。from config import DEBUG 在导入的那一刻,就把 config.DEBUG 当时的值复制了一份,绑定到了当前模块的 DEBUG 这个名字上。之后 config.DEBUG 再怎么变,当前模块的 DEBUG 都不会跟着变,因为它只是一个独立的变量,指向最初的那个 False。
这个坑的根源,是没搞清楚 import module 和 from module import name 在“绑定什么”上的本质区别。
先看一个最直观的对比
# 方式一:import module
import config
print(config.DEBUG) # False
config.DEBUG = True
print(config.DEBUG) # True
# 方式二:from module import name
from config import DEBUG
print(DEBUG) # False
config.DEBUG = True
print(DEBUG) # False,不变
print(config.DEBUG) # True
import config 把整个模块对象绑定到 config 这个名字上。你通过 config.DEBUG 访问的是模块对象的属性,每次访问都是实时读取。
from config import DEBUG 把模块里的 DEBUG 属性值绑定到当前模块的 DEBUG 这个名字上。绑定发生在导入的那一刻,之后两边就是独立的变量了。
用一个生活场景来理解。你有一个仓库,里面放着一批货物。
import module 的做法是:你拿到了仓库的钥匙。每次要取货,你都亲自去仓库里拿。仓库里货变了,你下次去拿就是新的。
from module import name 的做法是:你从仓库里取了一件货,放在自己家里。之后仓库里那件货再怎么换,你家里的还是当初取出来的那一件。
区别一目了然:一个是拿钥匙,随时去仓库取;一个是拿实物,拿了就固定了。
import module:绑定模块对象
import config 执行时,Python 做了两件事:
第一,找到 config.py,执行它,创建一个模块对象,模块对象里包含了 config.py 中定义的所有顶层名字(DATABASE_URL、REDIS_URL、DEBUG 等)。
第二,把当前模块的 config 这个名字绑定到这个模块对象上。
之后你写的 config.DEBUG,本质上是“在当前模块的命名空间里找到 config 这个名字,它指向一个模块对象,然后访问这个模块对象的 DEBUG 属性”。
每次访问 config.DEBUG,都是在模块对象的 __dict__ 里查一次。模块对象的属性变了,查到的值就变了。
import config
print(config.DEBUG) # False
config.DEBUG = True # 修改模块对象的属性
print(config.DEBUG) # True,实时反映
from module import name:绑定属性值
from config import DEBUG 执行时,Python 做了三件事:
第一,找到 config.py,执行它,创建模块对象。
第二,从模块对象里取出 DEBUG 属性的值。
第三,把当前模块的 DEBUG 这个名字绑定到这个值上。
注意第三步:绑定的是值,不是模块对象,也不是属性的引用。DEBUG 这个名字直接指向 False 这个布尔对象。
之后 config.DEBUG 再被修改,修改的是模块对象的属性,和当前模块的 DEBUG 这个名字没有任何关系。它们指向的是两个不同的地方。
from config import DEBUG
print(DEBUG) # False
config.DEBUG = True # 修改的是模块对象的属性
print(DEBUG) # False,当前模块的 DEBUG 还是指向最初的 False
坑一:以为 from import 导入的是“引用”
很多人把 from config import DEBUG 理解成“把 config.DEBUG 这个引用拿过来”,以为改了一边另一边也会变。实际上拿到的是值的一份绑定,不是引用。
对于不可变对象(int、str、bool、None),这个区别特别明显:
# config.py
DEBUG = False
TIMEOUT = 30
# main.py
from config import DEBUG, TIMEOUT
# 修改 config 里的值
import config
config.DEBUG = True
config.TIMEOUT = 60
print(DEBUG) # False,没变
print(TIMEOUT) # 30,没变
对于可变对象(列表、字典),情况稍微复杂一点:
# config.py
ALLOWED_HOSTS = ["localhost"]
# main.py
from config import ALLOWED_HOSTS
# 修改列表内容
ALLOWED_HOSTS.append("example.com")
print(ALLOWED_HOSTS) # ["localhost", "example.com"]
# 但重新赋值不会影响
import config
config.ALLOWED_HOSTS = ["newhost"]
print(ALLOWED_HOSTS) # 还是 ["localhost", "example.com"]
因为 ALLOWED_HOSTS 这个名字绑定的是列表对象,append 是修改列表对象本身,所以两边都能看到。但 config.ALLOWED_HOSTS = [...] 是重新绑定模块对象的属性,当前模块的 ALLOWED_HOSTS 还指向旧列表,看不到变化。
这个区别让“可变对象”和“不可变对象”在 from import 下表现不一致,更容易让人困惑。
坑二:循环导入
import module 和 from module import name 在循环导入时的行为也不同。
假设有两个模块互相导入:
# a.py
from b import func_b
def func_a():
func_b()
# b.py
from a import func_a
def func_b():
func_a()
运行 a.py,会报错:
ImportError: cannot import name 'func_b' from partially initialized module 'b'
因为 a.py 执行到 from b import func_b 时,开始执行 b.py。b.py 执行到 from a import func_a 时,a.py 还没执行完,func_a 还没定义,所以报错。
如果改成 import:
# a.py
import b
def func_a():
b.func_b()
# b.py
import a
def func_b():
a.func_a()
这样能跑。因为 import b 只要求 b 模块对象存在,不要求 b 里的某个名字已经定义。func_a 里调用 b.func_b() 是在函数执行时才解析的,那时候 b.py 已经执行完了。
from module import name 在导入时就需要名字存在,import module 只要求模块对象存在。循环导入时,后者更宽容。
但循环导入本身是设计问题,最好的解决方案是重构代码,消除循环依赖。import 能跑只是“碰巧”,不是推荐做法。
坑三:from module import * 污染命名空间
from module import * 会把模块里所有不以 _ 开头的名字全部导入当前命名空间。这会导致两个问题。
第一,命名冲突:
# utils.py
def process(data):
return data.strip()
# main.py
from utils import *
def process(data):
return data.upper()
# 此时 process 是 main.py 里定义的,utils.process 被覆盖了
第二,你不知道导入了什么:
from utils import *
# 现在命名空间里多了哪些名字?不知道
# 有没有覆盖已有的名字?不知道
import * 在大型项目里应该避免。它让代码的可读性和可维护性变差,也容易引入难以发现的 bug。
如果需要导入多个名字,显式列出:
from utils import process, validate, format_output
或者用 import 加前缀:
import utils
utils.process(data)
坑四:重新加载模块时 from import 的旧引用
Python 的 import 有缓存机制。一个模块第一次被导入后,会被放进 sys.modules,之后再导入直接拿缓存,不会重新执行。
import config
import config # 第二次直接拿缓存,不重新执行
如果你修改了 config.py 的内容,想让 Python 重新加载,需要 importlib.reload:
import importlib
import config
importlib.reload(config)
但 from config import DEBUG 导入的名字不会因为 reload 而更新:
from config import DEBUG
import config
import importlib
# 修改 config.py 文件内容
importlib.reload(config)
print(config.DEBUG) # 新值
print(DEBUG) # 旧值,没更新
因为 DEBUG 是当前模块的一个独立变量,reload 只重新执行了 config.py,更新了 config 模块对象的属性,没有动当前模块的 DEBUG。
坑五:模块名和变量名冲突
import json
json = json.dumps({"a": 1}) # 把 json 模块覆盖成了字符串
json.loads(...) # AttributeError: 'str' object has no attribute 'loads'
import json 把 json 这个名字绑定到模块对象。后面你又把 json 赋值成字符串,模块对象就丢了。
用 from import 可以避免这个问题,因为导入的是模块里的具体名字,不是模块名本身:
from json import dumps, loads
result = dumps({"a": 1})
data = loads(result)
但 from import 也有自己的冲突问题:如果模块里有个名字和你当前模块的变量重名,导入会覆盖你的变量。
什么时候用 import,什么时候用 from import?
判断标准:
需要频繁使用模块里的多个名字:用 import module,然后 module.func()。前缀让代码来源清晰。
只需要模块里的一个或几个名字:用 from module import name。代码更短,可读性更好。
需要修改模块的全局状态(比如配置、开关):用 import module,通过 module.var = value 修改。用 from import 拿到的只是快照。
可能循环导入:用 import module,延迟解析,更宽容。但更好的做法是消除循环依赖。
标准库或第三方库的常用函数:用 from import。比如 from pathlib import Path、from collections import defaultdict。
大型项目里的内部模块:优先用 import module,避免命名冲突,也让依赖关系更清晰。
**需要 import ***:几乎永远不要用。除非是交互式环境或者 __init__.py 里明确设计的公开接口。
一个折中方案:import module as alias
如果模块名太长,可以用 as 起别名:
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
别名让代码更短,同时保留了 module.func() 的清晰来源。这是大型项目里常见的做法。
from import 也支持 as:
from config import DATABASE_URL as DB_URL
from utils import process as process_data
别名可以解决命名冲突,也可以让名字更贴合当前模块的语义。
回到小陈的故事
小陈最终的方案是:所有需要动态修改的配置,统一用 import config 访问。
# order_service.py
import config
def query_order(order_id):
if config.DEBUG:
print(f"查询订单: {order_id}")
# 使用 config.DATABASE_URL 连接数据库
运维在启动脚本里改 config.DEBUG = True,order_service 立刻能看到。因为每次 config.DEBUG 都是实时读取模块对象的属性,不存在快照问题。
对于不需要动态修改的常量,他还是用 from config import DATABASE_URL,代码更短。但他加了一条注释:“这个值在启动时固定,运行时不改。”
他后来在团队文档里写了一条:“需要运行时修改的配置,用 import module,通过 module.var 访问。不需要修改的常量,可以用 from module import name。from import 拿到的是导入那一刻的值,不是引用,模块里后来改了,你这边看不到。循环导入时 import 更宽容,但最好从设计上消除循环依赖。import * 能不用就不用。”
这个区分看起来简单,但真到写代码的时候,很多人会随手写 from config import DEBUG,然后在需要动态改配置时发现改了不生效。或者用 from utils import *,然后在某个函数被莫名其妙覆盖时找不到原因。
记住那个仓库的比喻:import module 是拿钥匙,随时去仓库取最新的货;from module import name 是从仓库拿一件货回家,仓库里再换货,你家里的不会变。你要实时性,就拿钥匙;你确定这件货不会变,拿回家也无妨。
导入本身不难,难的是想清楚“这个值是固定的还是会变的、这个模块会不会和别的模块循环依赖、这个名字会不会冲突”。想清楚了,import 和 from import 就不再是坑,而是两个各有适用场景的工具。