代码语言

知识点思维导图

29 个知识节点

Python(08) - 模块与包管理

读完后,你应能完成以下任务:

  • 绘制“Python(08) - 模块与包管理 / 先建立直觉:模块 ≈ 一个 JS 文件”的关键对象与数据流,解释“类比:Python 里一个 .py 文件就是一个模块(module),等价于前端一个 .js / .ts 文件。”,并用源码位置、日志或 Trace 标注证据。
  • 为“Python(08) - 模块与包管理 / import 的几种写法(对照 JS)”设计正常与异常输入,验证“边界:from module import 对应不了 JS 里任何好习惯,等于把别人模块的所有名字一股脑塞进你的作用域,极易和你已有的变量撞名。”,输出首个偏差位置与回归测试结果。
  • 实现“Python(08) - 模块与包管理 / name == "main":判断「是被运行还是被导入」”的最小代码或配置,检验“这是 Python 新手第一眼最懵、但其实你在 node 里见过的东西。”,输出命令、结果与 Diff,并说明不适用边界。

你在前端里 import { foo } from './bar' 已经做了上千次。Python 的 import 长得几乎一样,但「什么算导出」「文件怎么被找到」「文件夹怎么变成包」这三件事和 JS 差得不少。这篇就把这些差异讲清楚,省得你被 ModuleNotFoundError 反复折磨。

一、先建立直觉:模块 ≈ 一个 JS 文件

类比:Python 里一个 .py 文件就是一个模块(module),等价于前端一个 .js / .ts 文件。文件名就是模块名。

并排看 JS,你会觉得很眼熟:

// utils.js
export const PI = 3.14
export function add(a, b) { return a + b }

// main.js
import * as utils from './utils.js'
console.log(utils.PI)
console.log(utils.add(1, 2))

边界(这里和 JS 不一样):Python 没有 export 关键字。模块里所有顶层定义的名字(变量、函数、类)自动都能被 import,不需要显式标记导出。换句话说,JS 是「默认私有,显式 export」,Python 是「默认全公开」。想表达「这个别导出」只能靠约定——名字前加下划线 _internal,但那只是约定,挡不住硬 import。


二、import 的几种写法(对照 JS)

JS / TS Python 说明
import * as utils from './utils' import utils 拿整个模块,用 utils.add 访问
import { add } from './utils' from utils import add 只拿某个名字,直接用 add
import { add, PI } from './utils' from utils import add, PI 拿多个
import { add as plus } from './utils' from utils import add as plus 重命名
import _ from 'lodash'(默认导出) (无对应概念) Python 没有「默认导出」
import * as np from 'numpy'(整模块起别名) import numpy as np 给整个模块起别名

代码示例:

边界from module import * 对应不了 JS 里任何好习惯,等于把别人模块的所有名字一股脑塞进你的作用域,极易和你已有的变量撞名。除了交互式调试,几乎不要用。


三、name == "main":判断「是被运行还是被导入」

这是 Python 新手第一眼最懵、但其实你在 node 里见过的东西。

类比:node 里判断「这个文件是被直接 node xxx.js 运行,还是被别人 require」用的是 require.main === module。Python 用的是 __name__ 这个内置变量。

并排看 node:

// app.js
function main() { console.log("程序启动") }

// 直接 node app.js 运行时为 true;被 require 时为 false
if (require.main === module) {
    main()
}

为什么需要它:因为 Python 的 import完整执行被导入文件的所有顶层代码(这点和 JS 一样,模块只在首次导入时执行一次)。如果你在文件顶层直接写了 main(),那别人 import 你这个模块时,main() 也会被意外执行。用 if __name__ == "__main__": 把入口逻辑包起来,就能区分「运行」和「被导入」两种场景。


四、包(package):文件夹 + init.py

类比:当模块多了要分目录管理时,一个文件夹就是一个包(package),约等于前端里一个带 index.js 入口的目录。

目录结构:

myapp/
├── __init__.py          # 有它,myapp 才是一个「包」(角色 ≈ index.js)
├── utils.py             # 子模块 myapp.utils
└── db/
    ├── __init__.py
    └── mysql.py         # 子模块 myapp.db.mysql

导入时用点 . 表示层级,对应 JS 的路径斜杠 /

4.1 init.py 是干嘛的?

它是这个包的「入口文件」,包被导入时它会先执行。最常见的两个用途:

这和前端的 barrel 文件(index.tsexport { connect } from './db/mysql' 收口再统一导出)是同一个思路。

边界(重要):从 Python 3.3 起,不写 __init__.py 的文件夹也能被当包导入(叫「命名空间包」)。所以你可能见过没有 __init__.py 的目录也能 import。但建议还是显式建一个空的 __init__.py——它让「这是一个正式的包」这件事清晰无歧义,也避免一些工具(测试发现、打包)出问题。把它当成「目录的 package.json 占位」就行。


五、模块查找路径:Python 是怎么找到文件的?

这是踩坑重灾区,必须讲透。前端 import './utils'相对当前文件找;而 Python 的 import utils 默认是在一组固定的搜索路径里找,机制不同。

类比:node 解析 require('lodash') 时会顺着一层层 node_modules 往上找。Python 也有一个「往哪找」的列表,叫 sys.path

sys.path 大致按这个顺序:

  1. 当前运行脚本所在的目录(你 python xxx.py,xxx.py 所在目录)
  2. 环境变量 PYTHONPATH 指定的目录
  3. 标准库目录(mathjson 这些在这)
  4. 第三方包安装目录 site-packages(pip 装的东西在这,角色 ≈ node_modules
概念 node / npm Python
第三方包装哪 node_modules/ site-packages/
装包命令 npm install x pip install x
搜索路径列表 逐层 node_modules sys.path
依赖清单 package.json requirements.txt / pyproject.toml
隔离环境 每个项目独立 node_modules 虚拟环境 venv(见下)

边界(最容易栽的坑):JS 里 import './utils'./ 是「相对文件」找;Python 里 import utils(不带点)是「在 sys.path 里找」,不是相对当前文件。所以同目录下的两个文件,A 想 import 同级的 B,能成功往往是因为「当前脚本目录」恰好在 sys.path 里——一旦你换个目录运行、或者把文件挪进包里,立刻 ModuleNotFoundError。Python 的「相对当前文件」要用下面的相对导入语法。


六、相对导入 vs 绝对导入

包内部的模块互相引用,有两种写法:

点的含义和文件路径完全对应:

文件路径写法 Python 相对导入 含义
./other from .other import x 同一个包里的兄弟模块
../utils from ..utils import x 上一级包里的模块
../../core from ...core import x 上两级

边界(高频报错):相对导入只能在「包内部、且该文件是被当作包的一部分导入」时使用。如果你直接 python myapp/db/mysql.py 去运行一个含相对导入的文件,会报 ImportError: attempted relative import with no known parent package。原因是直接运行时,这个文件被当成顶层脚本(__name____main__),它「不知道自己属于哪个包」,.. 就没有参照物。

解决办法:从项目根目录用 -m 模块方式运行,让 Python 知道包结构:

# 不要这样直接跑含相对导入的文件
python myapp/db/mysql.py        # ❌ 可能报 attempted relative import

# 用 -m,以「模块」方式从根目录运行,包结构才完整
python -m myapp.db.mysql        # ✅ 注意是点不是斜杠,且不带 .py

新手建议:项目内部统一用绝对导入from myapp.utils import log),心智负担最小,能绕开绝大多数相对导入的坑。


七、虚拟环境与装包(venv + pip)

最后补一块前端直觉对照。前端每个项目有自己的 node_modules,天然隔离。Python 默认所有项目共用一套全局环境,容易版本打架,所以要手动建**虚拟环境(venv)**来隔离。

# 建一个虚拟环境,会生成一个 venv 目录(角色 ≈ 项目专属的 node_modules + node 副本)
python -m venv venv

# 激活它(之后的 pip/python 都作用在这个隔离环境里)
source venv/bin/activate        # macOS / Linux
# venv\Scripts\activate         # Windows

# 装包,等价于 npm install
pip install requests

# 导出当前依赖清单,等价于 package.json 的 dependencies
pip freeze > requirements.txt

# 别人拿到项目后一键还原依赖,等价于 npm install
pip install -r requirements.txt
npm pip / venv
node_modules/ venv/(激活后包装进里面)
npm install x pip install x
package.json requirements.txt / pyproject.toml
npm install(还原) pip install -r requirements.txt
nvm 切 node 版本 不同 venv 用不同 Python

边界node_modules 是自动按目录隔离的,你几乎不用想;venv 需要你手动建 + 手动激活,忘了激活就会装到全局环境去。养成进项目先 source venv/bin/activate 的习惯。


八、常见踩坑清单

  1. 以为有 export:Python 顶层名字全自动可导入,没有 export;想标记私有用 _前缀(仅约定)。
  2. import utils 找不到:不带点的 import 走 sys.path,不是相对当前文件。同级文件互引在包里要用绝对/相对导入。
  3. 直接跑含相对导入的文件报错attempted relative import —— 改用 python -m 包.模块 从根目录运行。
  4. 循环导入(circular import):A 导 B、B 又导 A,可能拿到「还没初始化完」的半成品。解法:把 import 挪到函数内部,或拆分公共部分到第三个模块。和前端循环依赖一个道理。
  5. 改了模块代码没生效:模块只在首次 import 时执行一次并缓存(在 sys.modules 里)。普通脚本重新运行即可;交互环境/notebook 里要重启内核或用 importlib.reload

九、总结

  • 先建立直觉:模块 ≈ 一个 JS 文件:类比:Python 里一个 .py 文件就是一个模块(module),等价于前端一个 .js / .ts 文件。
  • import 的几种写法(对照 JS):边界:from module import 对应不了 JS 里任何好习惯,等于把别人模块的所有名字一股脑塞进你的作用域,极易和你已有的变量撞名。
  • name == "main":判断「是被运行还是被导入」:这是 Python 新手第一眼最懵、但其实你在 node 里见过的东西。
  • 包(package):文件夹 + init.py:类比:当模块多了要分目录管理时,一个文件夹就是一个包(package),约等于前端里一个带 index.js 入口的目录。
  • 模块查找路径:Python 是怎么找到文件的?:当前运行脚本所在的目录(你 python xxx.py,xxx.py 所在目录) -> 环境变量 PYTHONPATH 指定的目录 -> 标准库目录(math、json 这些在这) -> 第三方包安装目录 site-packages(pip 装的东西在这,角色 ≈ node_modules)
  • 相对导入 vs 绝对导入:边界(高频报错):相对导入只能在「包内部、且该文件是被当作包的一部分导入」时使用。

学完自测

选择所有正确答案;提交后逐项核对判断依据。

1在“模块与包管理”中,需要同时满足“先建立直觉:模块 ≈ 一个 JS 文件”与“import 的几种写法(对照 JS)”。给定正文约束“模块里所有顶层定义的名字(变量、函数、类)自动都能被 import,不需要显式标记导出。”,哪些判断保持了原有处理机制?多选
2“模块与包管理”出现偏差:“在“模块与包管理 / name == "main":判断「是被运行还是被导入」”中,即使不满足“因为 Python 的 import 会完整执行被导入文件的所有顶层代码(这点和 JS 一样,模块只在首次导入时执行一次)”,结果与副作用仍会保持不变。”已成为实际行为。围绕“name == "main":判断「是被运行还是被导入」”与“包(package):文件夹 + init.py”,哪些判断能定位被改变的职责或边界?多选
3评审“模块与包管理”方案时,验收条件包含“所以你可能见过没有 init.py 的目录也能 import。”。关于“init.py 是干嘛的?”与“模块查找路径:Python 是怎么找到文件的?”的哪些决策符合正文机制?多选