这篇只解决一个问题:Python 里的模块、包、导入方式和 import 查找规则到底是什么关系。
先按前端思维类比:
shell
JavaScript / TypeScript
-> 一个文件可以 export
-> 另一个文件 import
-> npm install 安装依赖
-> import xxx from "包名"
Python
-> 一个 .py 文件就是模块
-> 另一个 .py 文件 import
-> pip install 安装依赖
-> import 模块名
但 Python 和前端最大的区别是:
arduino
Python 没有 export 关键字
Python 文件里写在顶层的变量、函数、类,默认就可以被其他文件导入。
模块概述
模块就是一个 .py 文件。
一个模块里可以放很多内容:
- 变量
- 函数
- 类
- 配置常量
- 一段可以被复用的业务逻辑
比如:
python
user_service.py
-> get_user_name()
-> create_user()
-> UserService
把同一类能力集中放到一个模块里,可以减少命名冲突,也方便后面复用和维护。
模块的分类
Python 里的模块可以分成几类:
| 模块来源 | 例子 | 怎么得到 |
|---|---|---|
| 内置模块 | sys、builtins |
Python 解释器自带,有些没有真实 .py 文件 |
| 标准库模块 | os、json、datetime、pathlib |
安装 Python 时自带,不需要 pip install |
| 自定义模块 | user_service.py、app/services/order.py |
你自己项目里的 .py 文件 |
| 第三方模块 | requests、dotenv、pandas |
通过 pip install 安装到当前环境 |
模块命名注意点
模块名就是 .py 文件名去掉 .py 后的名字。
命名时先记三点:
- 符合 Python 标识符命名规则:字母、数字、下划线,不能用数字开头。
- 区分大小写:
user.py和User.py不是一个模块名。 - 不要和标准库、第三方库重名,比如
json.py、random.py、requests.py、dotenv.py。
新手阶段建议统一使用小写字母和下划线:
python
user_service.py
order_service.py
database_config.py
常用标准库模块
标准库是跟着 Python 一起安装的模块,不需要单独 pip install。
| 场景 | 常用模块 |
|---|---|
| 拷贝对象 | copy |
| 操作系统、路径、文件夹 | os、pathlib、shutil |
| 随机数和随机选择 | random |
| 时间、延时、格式化时间 | time、datetime |
| 数学计算 | math、decimal |
| Python 解释器相关 | sys |
| 文件和数据 | json、csv、sqlite3、pickle |
| 文本和集合 | re、string、collections、itertools |
| 网络和调试 | urllib、http、logging、pdb、unittest |
有一些模块属于内置模块,它们由解释器直接提供,不一定能看到具体源码文件,也可能没有 __file__ 属性。
比如:
python
import sys
print(sys.__file__) # 有些环境下会报错或没有这个属性
新手阶段有一个判断顺序:
rust
先看标准库能不能做
-> 标准库不够,再找第三方包
-> 项目自己的代码,再按包结构拆模块
一、一个 .py 文件就是一个模块
项目结构:
python
demo/
-> main.py
-> user_service.py
user_service.py:
python
def get_user_name(user_id: int) -> str:
return f"user-{user_id}"
main.py:
python
from user_service import get_user_name
name = get_user_name(1)
print(name)
这里的理解是:
text
user_service.py 文件
-> 模块名叫 user_service
-> get_user_name 是这个模块里的函数
-> main.py 可以从 user_service 里导入 get_user_name
前端类比:
js
import { getUserName } from "./user-service";
Python 不需要在 user_service.py 里写:
python
export get_user_name
只要函数写在模块顶层,就能被导入。
二、Python 的几种导入方式
截图里提到的模块导入方式,可以先按这五种记:
python
# 1. 导入整个模块
import user_service
# 2. 导入整个模块,并起别名
import user_service as user
# 3. 从模块中导入具体内容
from user_service import get_user_name, create_user
# 4. 从模块中导入具体内容,并起别名
from user_service import get_user_name as get_name
# 5. 星号导入,不推荐新手常用
from user_service import *
导入整个模块
python
import user_service
name = user_service.get_user_name(1)
print(name)
这种方式更清楚:函数来自 user_service 模块。
从模块里导入某个名字
python
from user_service import get_user_name
name = get_user_name(1)
print(name)
这种方式更简洁:后面可以直接用 get_user_name。
导入时起别名
python
import user_service as user
name = user.get_user_name(1)
print(name)
常见库也会这么写:
python
import pandas as pd
import numpy as np
不推荐随便使用星号导入
python
from user_service import *
这会把模块里的很多名字都放到当前文件里,阅读代码时不容易看出变量从哪里来。新手阶段不建议这么写。
三、包:多个模块组成一个目录
包就是用来管理多个模块的目录。
当文件越来越多时,一般会按目录组织。
text
demo/
-> main.py
-> services/
-> __init__.py
-> user_service.py
services/user_service.py:
python
def get_user_name(user_id: int) -> str:
return f"user-{user_id}"
main.py:
python
from services.user_service import get_user_name
name = get_user_name(1)
print(name)
这里的理解是:
text
services 是包
user_service.py 是 services 包里的模块
get_user_name 是 user_service 模块里的函数
所以导入路径是:
python
from services.user_service import get_user_name
__init__.py 可以先理解成:
告诉 Python:这个目录可以当成一个包来使用
现代 Python 里,有些场景不写 __init__.py 也能工作。但新手阶段建议保留,因为它能让包结构更明确,也能减少一些工具识别问题。
包和模块的关系
可以这样记:
模块:一个 .py 文件
包:管理模块的目录
一个包里可以有多个模块,也可以继续放子包:
rust
trade/
-> __init__.py
-> order.py
-> pay.py
-> refund/
-> __init__.py
-> audit.py
这里:
css
trade 是包
order.py 是 trade 包里的模块
pay.py 是 trade 包里的模块
refund 是 trade 下面的子包
audit.py 是 refund 子包里的模块
包的分类
和模块类似,包也可以分成:
| 包来源 | 例子 |
|---|---|
| 标准库包 | Python 自带的包,比如 email、http、xml |
| 自定义包 | 项目里自己写的 services/、trade/、app/ |
| 第三方包 | pip install 安装来的 requests、fastapi、dotenv |
包命名注意点
- 包名也要符合标识符命名规则。
- 包名区分大小写。
- 建议全部使用小写字母。
- 不要和标准库包、第三方包同名。
关于 __init__.py
__init__.py 是包的初始化文件。
新手阶段先记这几件事:
- 目录里有
__init__.py,这个目录就更明确地表示"我是一个包"。 - 包被导入时,
__init__.py会被执行。 __init__.py里可以写包的初始化逻辑,但不要写太重的业务代码。__init__.py里定义的名字,可以被from 包名 import 名字导入。__init__.py里也可以写__all__,控制from 包名 import *能导入哪些名字。
比如:
rust
trade/
-> __init__.py
-> order.py
-> pay.py
trade/__init__.py:
python
from .order import create_order
from .pay import wechat_pay
__all__ = ["create_order", "wechat_pay"]
其他文件就可以写:
python
from trade import create_order, wechat_pay
包的常见导入方式
截图里提到的包导入方式,可以按这几种理解:
python
# 1. 导入包里的某个模块
import trade.order
trade.order.create_order()
# 2. 导入包里的某个模块,并起别名
import trade.order as order
order.create_order()
# 3. 从包里的模块导入具体内容
from trade.order import create_order
create_order()
# 4. 从包里的模块导入具体内容,并起别名
from trade.order import create_order as create
create()
# 5. 从包里的模块星号导入,不推荐新手常用
from trade.order import *
# 6. 从包里导入模块
from trade import order, pay
order.create_order()
pay.wechat_pay()
# 7. 从包里导入模块,并起别名
from trade import order as o, pay as p
o.create_order()
p.wechat_pay()
# 8. 从包里星号导入,受 __init__.py 里的 __all__ 影响
from trade import *
实际项目里最推荐先用两种:
python
from trade.order import create_order
from trade import order
它们来源清楚,阅读成本低。
四、Python 的"导出"怎么理解
Python 没有像 JS 这样的显式导出:
js
export function getUserName() {}
Python 更像是:
arduino
模块顶层定义的名字
-> 其他模块原则上都可以 import
比如 config.py:
python
APP_ENV = "development"
def get_database_url() -> str:
return "postgresql://localhost:5432/app"
class UserConfig:
pass
其他文件可以导入:
python
from config import APP_ENV, UserConfig, get_database_url
所以你可以把 Python 的"导出"理解成:
rust
写在模块顶层
-> 就相当于可以被别人导入
下划线开头表示内部使用
python
def _format_user_name(name: str) -> str:
return name.strip()
以下划线开头的名字不是绝对不能导入,而是告诉别人:
text
这是模块内部实现,外部不建议依赖
这更像一种约定。
all 可以控制星号导入
python
__all__ = ["get_user_name"]
def get_user_name(user_id: int) -> str:
return f"user-{user_id}"
def _format_user_name(name: str) -> str:
return name.strip()
当别人写:
python
from user_service import *
只会导入 __all__ 里列出来的名字。
但日常开发里,不建议依赖 from xxx import *。先知道 __all__ 存在即可。
name 用来判断模块是被直接运行,还是被导入
每个 Python 模块都有一个内置变量:
text
__name__
它的值取决于模块的运行方式。
如果这个文件是作为主程序直接运行:
python
python user_service.py
那么在 user_service.py 里:
python
print(__name__) # "__main__"
如果这个文件是被别的文件导入:
python
import user_service
那么 user_service.py 里的 __name__ 通常是模块名:
text
user_service
所以常见写法是:
python
def main() -> None:
print("直接运行这个文件时,才执行这里")
if __name__ == "__main__":
main()
意思是:
text
直接运行当前文件
-> 执行 main()
被其他文件 import
-> 不执行 main()
这样可以避免一个模块被导入时,顺手执行了测试代码、打印代码或临时脚本逻辑。
五、Python import 时到底在找什么
这一节只讲查找规则。
前端里你写:
js
import { getUserName } from "./services/user-service";
这个 ./ 很明确:
text
从当前文件所在目录出发
-> 进入 services/
-> 找 user-service
Python 的常规 import 不是这种规则。
Python 主要是:
text
不是从当前文件位置出发
而是从 sys.path 这些入口目录出发
先记这一句:
arduino
Python import 时,先找最左边那个名字。
比如:
arduino
import os
最左边的名字是:
lua
os
python
from app.services.user_service import get_user_name
最左边的名字是:
app
也就是说,from app.services.user_service import get_user_name 不是先找 get_user_name,而是先找 app。
sys.path 是入口目录列表
Python 从哪里找最左边那个名字?
答案是:
lua
sys.path
可以打印出来看:
python
import sys
for item in sys.path:
print(item)
sys.path 里放的不是最终文件,而是一组"入口目录"。
Python 会从这些入口目录开始找
常见入口来源先记这几个:
markdown
1. 入口脚本所在目录
2. PYTHONPATH 配置的目录
3. Python 标准库目录
4. 当前环境的 site-packages
例子一:找项目里的文件
项目结构:
rust
demo/
-> main.py
-> user_service.py
运行:
css
python main.py
这时入口脚本是:
bash
demo/main.py
所以 demo/ 会进入 sys.path。
代码里写:
arduino
import user_service
Python 查找的是最左边的 user_service。
它会从 sys.path 的入口目录里试:
bash
demo/user_service.py
demo/user_service/
因为项目里有:
bash
demo/user_service.py
所以能导入成功。
例子二:找多层路径
项目结构:
rust
demo/
-> main.py
-> app/
-> __init__.py
-> services/
-> __init__.py
-> user_service.py
运行:
css
python main.py
这时 demo/ 在 sys.path 里。
代码里写:
python
from app.services.user_service import get_user_name
Python 先找最左边的:
app
它会从入口目录里试:
bash
demo/app/
找到 demo/app/ 后,再继续按点号往下找:
bash
demo/app/services/
demo/app/services/user_service.py
最后才从 user_service.py 里拿:
get_user_name
所以这句的查找顺序是:
shell
sys.path 里的 demo/
-> 找 app
-> 找 app/services
-> 找 app/services/user_service.py
-> 从这个文件里取 get_user_name
例子三:为什么 import os 能找到
代码:
python
import os
Python 先找最左边的:
lua
os
它会从多个入口里找,其中包括 Python 标准库目录。
所以即使你的项目里没有 os.py,Python 也能在标准库里找到 os。
第三方库也是类似的。只要某个第三方库所在目录进入了 sys.path,Python 就能从那个入口目录里找到它。
Python 不是从当前文件相对查找
这点和前端差异最大。
假设项目结构:
rust
demo/
-> main.py
-> app/
-> services/
-> order_service.py
-> user_service.py
如果你在 order_service.py 里写:
python
import user_service
Python 不会自动从 order_service.py 所在目录开始找:
bash
demo/app/services/user_service.py
它还是从 sys.path 的入口目录开始找:
lua
sys.path 里的某个入口目录
-> user_service.py
如果 sys.path 里只有 demo/,那它会找:
bash
demo/user_service.py
demo/user_service/
不会自动找:
bash
demo/app/services/user_service.py
所以如果你想从项目根入口导入,应该写完整路径:
python
from app.services.user_service import get_user_name
这不是"文件相对路径",而是"从 sys.path 入口目录开始的模块路径"。
相对导入是另一套写法
Python 也有相对导入,但必须显式写点号:
javascript
from .user_service import get_user_name
这个点号 . 才表示:
从当前包开始找
如果要从上一级找:
arduino
from ..config import APP_ENV
这个 .. 才表示:
从上一级包开始找
注意:相对导入不是按照"当前文件路径"随便找,它要求当前文件处在包上下文里。
所以直接运行子文件时容易报错:
bash
python app/services/order_service.py
如果里面写了:
python
from .user_service import get_user_name
可能会报:
scala
ImportError: attempted relative import with no known parent package
原因是:你把它当普通脚本运行了,Python 不知道它属于 app.services。
运行方式会改变 sys.path
这也是 Python import 容易乱的原因。
项目结构:
rust
demo/
-> main.py
-> app/
-> services/
-> order_service.py
如果运行:
css
python main.py
入口脚本目录是:
demo/
所以 sys.path 里通常有 demo/。
如果运行:
bash
python app/services/order_service.py
入口脚本目录变成:
bash
demo/app/services/
所以 sys.path 的入口变了。原来能从 demo/ 找到的 app,现在可能反而找不到。
所以项目里更推荐从固定入口启动:
css
python main.py
或者用模块方式运行:
python -m app.services.order_service
怎么确认到底从哪里加载
可以看 __file__:
go
import json
print(json.__file__)
如果是你自己的模块:
go
import app.services.user_service
print(app.services.user_service.__file__)
有些内置模块或冻结模块没有普通的 __file__,这是正常的。
最后用一张规则表记:
shell
import user_service
-> 从 sys.path 入口里找 user_service.py 或 user_service/
from app.services.user_service import get_user_name
-> 从 sys.path 入口里找 app/
-> 再找 app/services/
-> 再找 app/services/user_service.py
-> 最后取 get_user_name
from .user_service import get_user_name
-> 从当前包里找 user_service
-> 需要当前文件处在包上下文里
这就是 Python 和前端相对路径导入最大的区别:
text
前端 ./xxx
-> 从当前文件位置找
Python import xxx
-> 从 sys.path 入口目录找
六、常见错误
文件名和要导入的名字重名
不要把自己的文件命名成:
text
requests.py
json.py
random.py
比如你自己写了一个 json.py,再执行:
python
import json
Python 可能会优先导入你本地的 json.py,而不是标准库里的 json,导致奇怪的错误。
循环导入
a.py:
python
from b import get_b
def get_a() -> str:
return "a"
b.py:
python
from a import get_a
def get_b() -> str:
return "b"
两个模块互相导入,容易出现初始化顺序问题。
新手阶段先记住:
text
公共函数放到更底层的 utils / services 里
不要让两个文件互相 import
七、先记住这几句话
- 一个
.py文件就是一个模块 - 一个目录加
__init__.py可以当成包 - 模块分为内置模块、标准库模块、自定义模块、第三方模块
- 包也分为标准库包、自定义包、第三方包
- Python 没有 export,顶层定义的名字默认可以被 import
__all__主要控制from xxx import *能导入哪些名字__name__可以判断模块是被直接运行,还是被 import- import 查找靠 sys.path,不是从当前文件一路往下扫