Python 模块与包的导入导出

这篇只解决一个问题: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 里的模块可以分成几类:

模块来源 例子 怎么得到
内置模块 sysbuiltins Python 解释器自带,有些没有真实 .py 文件
标准库模块 osjsondatetimepathlib 安装 Python 时自带,不需要 pip install
自定义模块 user_service.pyapp/services/order.py 你自己项目里的 .py 文件
第三方模块 requestsdotenvpandas 通过 pip install 安装到当前环境

模块命名注意点

模块名就是 .py 文件名去掉 .py 后的名字。

命名时先记三点:

  • 符合 Python 标识符命名规则:字母、数字、下划线,不能用数字开头。
  • 区分大小写:user.pyUser.py 不是一个模块名。
  • 不要和标准库、第三方库重名,比如 json.pyrandom.pyrequests.pydotenv.py

新手阶段建议统一使用小写字母和下划线:

python 复制代码
user_service.py
order_service.py
database_config.py

常用标准库模块

标准库是跟着 Python 一起安装的模块,不需要单独 pip install

场景 常用模块
拷贝对象 copy
操作系统、路径、文件夹 ospathlibshutil
随机数和随机选择 random
时间、延时、格式化时间 timedatetime
数学计算 mathdecimal
Python 解释器相关 sys
文件和数据 jsoncsvsqlite3pickle
文本和集合 restringcollectionsitertools
网络和调试 urllibhttploggingpdbunittest

有一些模块属于内置模块,它们由解释器直接提供,不一定能看到具体源码文件,也可能没有 __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 自带的包,比如 emailhttpxml
自定义包 项目里自己写的 services/trade/app/
第三方包 pip install 安装来的 requestsfastapidotenv

包命名注意点

  • 包名也要符合标识符命名规则。
  • 包名区分大小写。
  • 建议全部使用小写字母。
  • 不要和标准库包、第三方包同名。

关于 __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,不是从当前文件一路往下扫
相关推荐
研☆香1 小时前
es6新特性功能介绍(四)
前端·ecmascript·es6
微扬嘴角1 小时前
React篇1--JSX语法规则、组件、组件实例的3大特性
前端·react.js·前端框架
ice8130331811 小时前
【Python】Matplotlib折线图绘制
开发语言·python·matplotlib
夜微凉41 小时前
三、Spring
java·后端·spring
copyer_xyf1 小时前
Python venv 虚拟环境
前端·后端·python
无聊的老谢2 小时前
Vue 3 + TypeScript 构建大型电信运维平台的前端架构设计
前端·vue.js·typescript
xiaofeichaichai2 小时前
Map / Set / WeakMap / WeakSet
前端·javascript
李可以量化2 小时前
成交量的终极量化策略:价量共振指标完整实现(下篇)
前端·数据库·人工智能
林爷万福2 小时前
GitHub 开源光谱数据处理项目推荐
python·光纤光谱仪