前言
import是学 Python 最早接触的语句之一,但它恰恰是被「会用」和「懂」拉开距离的一条。多数人对它的理解停留在「把那个文件引进来」,于是遇到下面这些情况就卡住:明明改了代码为什么没生效、为什么同样的导入在脚本里能用、放进包里就报错、为什么两个文件互相导入就死锁。
本文不谈包的分发和目录组织,只谈一件窄一点的事:import这条语句本身在做什么。包括名字怎么绑定、模块什么时候执行、缓存放在哪、几种导入写法各有什么副作用。示例只依赖标准库,Python 3.8 及以上可运行。
一、import 绑定的是什么名字、执行几次
这是最容易出错、也最该先说清的一点。看四种写法各自绑定了什么:
# 适用于 Python 3.8+
import os.path
from os import path
from os import path as osp
from os.path import join| 写法 | 当前命名空间里新增的名字 | 类型 |
|---|
import os.path | os | 模块对象(顶层包) |
from os import path | path | 模块对象 |
from os import path as osp | osp | 模块对象 |
from os.path import join | join | 函数对象 |
最关键的一条:import a.b绑定的是a,不是a.b。这一点和第 8 个坑点里说的__import__("a.b")返回顶层模块是同一个道理。你之所以能写os.path.join(...),是因为导入系统在os模块对象上挂了属性path,而不是因为名字os.path被绑进了你的命名空间。
由此可以推出一个实用技巧:import numpy.linalg之后,你只能写numpy.linalg.solve(...),写linalg.solve(...)会报NameError(除非另外导入过)。想让短名字可用,就显式写from numpy import linalg。
模块只执行一次,副作用也只在第一次发生
import的第二件事是执行被导入模块的顶层代码。这句话有两个推论。
推论一:同一进程内,同一个模块的顶层代码只执行一次,之后都从缓存里拿。缓存是sys.modules这个普通字典:
# 适用于 Python 3.8+
import sys
before = "decimal" in sys.modules
import decimal
after = "decimal" in sys.modules
print(before, after) # False True
print(sys.modules["decimal"] is decimal) # True推论二:放在模块顶层的副作用会一直潜伏。比如某个模块顶层写了print("loading..."),只要有人导入它,屏幕上就多一行;而它在整个进程生命周期里最多出现一次。这解释了为什么很多库的import语句会打印版本号或者写日志——它们把初始化动作放在了顶层。
如果你希望某段代码只在直接运行这个文件时执行,用惯用法保护:
# 适用于 Python 3.8+
def main():
print("这里是主流程")
if __name__ == "__main__":
main()__name__的值取决于模块怎么被使用:作为脚本直接运行是字符串"__main__",被导入则是模块名。这不是约定,是导入系统写入的属性。
二、python -m与函数内导入
同一个文件,python foo.py和python -m foo跑出来可能不一样。差别在于解释器给模块设的元信息:
| 启动方式 | __name__ | __package__ | 相对导入是否可用 |
|---|
python pkg/mod.py | "__main__" | ""(空) | 不可用 |
python -m pkg.mod | "__main__" | "pkg" | 可用 |
被import pkg.mod | "pkg.mod" | "pkg" | 可用 |
也就是说,-m会正确设置包上下文,让包内的相对导入成立;直接给文件路径则不会,因为解释器只知道「一个脚本」,不知道它属于哪个包。这是「包里的模块能用from . import x,但单独跑它就报错」的根本原因。
顺带对比一下sys.path的第一项,两者也不同:
python pkg/mod.py:第一项是脚本所在目录pkg。python -m pkg.mod:第一项是当前工作目录。
所以同一条import util,两种启动方式找到的可能是两个不同的文件——一个在pkg/里,一个在当前目录里。这类「我本地好好的」问题,十有八九出在这里。
导入写在哪一行,是有区别的
import不一定非要写在文件顶部。写在函数内部完全合法,而且有大用处。
用途一:避开循环导入。两个模块互相依赖时,把其中一方的导入推迟到函数调用时,可以打破死锁。
# 适用于 Python 3.8+
# 文件名 service.py
def run():
# 在函数内导入:此时两个模块都已完成初始化
from . import repository
return repository.fetch_all()用途二:避免把可选的重量级依赖变成硬性要求。
# 适用于 Python 3.8+
def to_json(data):
"""把数据转成 JSON;没装第三方加速库就走标准库。"""
try:
import orjson as _json # 假设存在,未安装则回退
except ImportError:
import json as _json
return _json.dumps(data)标准库json一定存在,所以except分支总能兜住;外面那个名字是假设存在的可选依赖,既然拿不准它的 API,就以它自己的官方文档为准。这种「可选加速器」模式很常见,但要注意:导入失败被吞掉的时候,别把真正的错误也一起吞了。用except ImportError而不是裸的except:,原因见坑点第 6 条。
用途三:缩短启动时间。如果一个子命令只在特定场景用得到,把它的导入放进那个分支里,可以让另外九成调用都不付这份代价。这是把「模块顶层不做重活」这条原则贯彻到底的做法。
需要提醒的是:别把这当成随手可用的技巧。导入位置会显著影响可读性,放在顶部的导入一眼可见,藏在函数里的则需要追代码。用的时候最好配一行注释说明原因。
三、动态导入:把模块名当数据用
有时候模块名不是写死的,而是从配置、命令行参数或环境变量里读出来的。这时用importlib.import_module(name):
# 适用于 Python 3.8+
import importlib
def load_backend(name):
"""按名字加载后端模块,名字非法时抛出 ImportError。"""
if not name.isidentifier():
raise ValueError(f"非法的模块名:{name!r}")
return importlib.import_module(f"backends.{name}")
# 调用示意(前提是存在 backends 这个包,且其中有 memory 模块)
# mod = load_backend("memory")
# 此时 mod 就是 backends.memory 模块本身三个要点:import_module的参数是字符串;返回值是目标模块本身(不是像__import__那样返回顶层包);它走的还是同一套机制,所以sys.modules缓存照样生效。在拿外部输入拼模块名之前,先校验一下名字是否合法,避免把任意输入直接喂给导入系统。
要判断某个模块能否被找到(而不是立刻导入它),用importlib.util.find_spec(name),找不到时返回None:
# 适用于 Python 3.8+
import importlib.util
for name in ["json", "definitely_not_a_module_xyz"]:
spec = importlib.util.find_spec(name)
print(name, "->", None if spec is None else spec.origin)注意find_spec只回答「找得到吗」,不回答「导入一定成功吗」——模块自己依赖的东西缺失时,导入仍会失败。
四、from m import *为什么不建议用
from m import *把对方模块里所有不以单下划线开头的顶层名字倒进当前命名空间。三个具体危害:
- 覆盖。你本地的
list、id、open之类的名字可能被静默替换,之后的行为完全变了,而且没有任何提示。 - 来源不明。读代码看到
process(),无法判断它来自哪个模块。 - 受
__all__支配。对方模块如果定义了__all__,实际导入的名字会跟着变,你的代码在一个小版本升级后就可能少了个名字。
真要写,只能写在一个包的__init__.py里配合__all__来控制对外接口,其余场景一律显式列出名字。
常见坑点
1. 以为import a.b绑定了a.b
❌ 写import numpy.linalg之后直接调linalg.solve(...),报NameError。 ✅ 记住绑定的是顶层名字numpy,要么写全numpy.linalg.solve(...),要么显式from numpy import linalg。
2. 用重复 import 来「刷新」模块
❌ 改了源码后在同一次解释器会话里再敲一次import mymod,期望看到新代码。 ✅ 模块只在首次导入时执行;改动要重启进程,交互式里可以用importlib.reload,但要清楚已持有的旧引用不会自动更新。
3. 把顶层导入当成「只在该函数里用」
❌ 写from heavy_lib import thing放在文件顶部却只在一个少用到的函数里调用它,每次启动都付出加载代价。 ✅ 把这种导入挪进函数内部,并加注释说明意图。
4. 用python pkg/mod.py跑包内模块
❌ 包内写from . import helpers,直接跑文件时报「没有已知的父包」。 ✅ 从包的父目录用python -m pkg.mod运行,让包上下文正确。
5. 用裸except:包裹导入
❌try: import x后面跟裸except:,连KeyboardInterrupt、SystemExit一起吞掉,程序按 Ctrl+C 都不停。 ✅ 只捕获ImportError,必要时加ModuleNotFoundError,绝不写裸except:。
6. 用__import__做动态导入
❌__import__("a.b")拿到的是顶层模块a,不是a.b。 ✅ 用importlib.import_module("a.b"),它返回目标模块本身。
7. 用外部输入直接拼模块名
❌ 直接把配置里的字符串塞进importlib.import_module,非法名字会抛出难懂的错误,也给了不该有的自由度。 ✅ 先用str.isidentifier()或白名单校验,再导入。
8. 沿用 Python 2 的导入写法
❌ 依赖隐式相对导入(在包里写import helpers找同目录)、把print当语句用。Python 2.7 已于2020 年 1 月 1 日停止维护。 ✅ 包内显式写from . import helpers;输出用print(...)。
总结
| 关注点 | 结论 |
|---|
| 绑定什么名字 | import a.b绑a;from a import b绑b |
| 执行几次 | 每个进程内每个模块只执行一次 |
| 缓存在哪 | sys.modules,一个普通字典 |
-m与直接运行 | -m设好__package__,相对导入才可用 |
| 导入的位置 | 顶部是默认;函数内导入可用于破循环、省启动时间 |
| 动态导入 | 用importlib.import_module,不要用__import__ |
把import理解成「绑定名字 + 执行一次 + 进缓存」这三件事,很多看似诡异的导入问题就都有了解释。下次遇到「改了不生效」「本地能用别处报错」「互相导入就崩」,不妨先问自己一句:它在绑定谁的名字、这段代码执行过几次、sys.path的第一项到底是哪个目录。