跳到主要内容

使用模块

模块(module)就是一个可以被其他 Python 代码使用的 .py 文件。它能把相关变量、函数和类组织在一起,避免所有代码堆在同一个文件中,也方便复用和测试。

本文使用 Python 3.10 及以上版本。读完后,你应该能够:

  • 创建并导入自己的模块;
  • 根据场景选择合适的导入写法;
  • 理解名称空间、模块查找、首次执行与导入缓存;
  • 使用 __name__ == "__main__" 区分直接运行和被导入;
  • 在包内正确使用绝对导入和相对导入;
  • 处理循环导入、名称冲突和模块找不到等常见问题。

从一个最小模块开始​

假设目录中有两个文件:

project/
├── calculator.py
└── main.py

在 calculator.py 中定义可复用的内容:

calculator.py
PI = 3.14159


def add(left, right):
return left + right


class Calculator:
def multiply(self, left, right):
return left * right

在 main.py 中导入并使用它:

main.py
import calculator

print(calculator.PI) # 3.14159
print(calculator.add(2, 3)) # 5

tool = calculator.Calculator()
print(tool.multiply(4, 5)) # 20

import calculator 会把模块对象绑定到当前文件的名称 calculator。模块中的成员通过 模块名.成员名 访问,这样可以清楚看出名称来自哪里。

模块名必须是合法的 Python 标识符才能使用普通 import 语句,因此文件名通常使用小写字母和下划线,例如 order_service.py,不要使用空格或连字符。

常用导入方式​

导入整个模块​

import math

print(math.sqrt(16)) # 4.0
print(math.pi)

这种写法保留模块前缀,来源最明确,也不容易和当前文件中的名称冲突,通常应优先使用。

一行可以导入多个模块,但分行更容易阅读:

import json
import pathlib

给模块设置别名​

import datetime as dt

today = dt.date.today()

别名适合名称较长或社区已有通用约定的模块。别名应简短但仍能表达含义,不要为了少写几个字符使用难懂的缩写。

从模块导入指定名称​

from math import pi, sqrt

print(sqrt(16))
print(pi)

被导入的名称会直接进入当前名称空间,不再需要写 math.。这种方式适合少量、含义明确且不易冲突的名称。

也可以给成员设置别名:

from pathlib import Path as FilePath

config_path = FilePath("config.json")

需要注意,from module import name 绑定的是导入当时的对象:

from settings import timeout
import settings

settings.timeout = 60
print(timeout) # 仍是导入时绑定的旧值
print(settings.timeout) # 60

如果需要始终通过模块读取最新状态,应导入模块本身并使用 settings.timeout。

避免星号导入​

from math import * # 不推荐

星号导入会把许多名称放进当前名称空间,读代码时难以判断它们来自哪里,还可能悄悄覆盖已有名称。普通业务代码应明确列出需要的名称。

模块可以通过 __all__ 指定星号导入时公开哪些名称,但 __all__ 并不能阻止使用者显式访问其他成员:

report.py
__all__ = ["create_report"]


def create_report():
return "report"


def _normalize_data():
return "normalized"

以下划线开头的名称表示“内部实现”,这是一种约定,不是强制访问控制。

导入语句做了什么​

第一次执行 import calculator 时,Python 大致会完成这些步骤:

  1. 在导入路径中查找名为 calculator 的模块或包;
  2. 创建模块对象;
  3. 从上到下执行模块顶层代码,建立模块自己的名称空间;
  4. 把模块对象记录到 sys.modules;
  5. 在当前名称空间中绑定名称 calculator。

“导入模块”不只是复制函数定义,模块顶层语句会被执行:

notice.py
print("正在加载 notice 模块")


def send():
print("发送通知")

同一个 Python 进程中,普通模块通常只在首次导入时执行一次:

import notice # 打印“正在加载 notice 模块”
import notice # 使用缓存,通常不会再次打印

因此,不要在模块顶层执行昂贵计算、发送网络请求或修改外部数据。导入应尽量轻量且可预测。确实需要初始化时,把操作放进函数并由调用方显式触发。

导入缓存与 sys.modules​

sys.modules 是“已加载模块名到模块对象”的字典:

import sys
import math

print(sys.modules["math"] is math) # True

缓存让多处导入共享同一个模块对象,也意味着模块级可变状态会被共享:

counter.py
value = 0


def increment():
global value
value += 1
return value

不同文件导入 counter 后操作的是同一份 value。共享状态会增加测试和并发处理的难度,应优先通过函数参数、返回值或对象管理状态。

开发时修改了模块源码,已经运行的进程不会自动重新执行它。交互式调试可以使用 importlib.reload(),但它可能保留旧对象引用;实际程序通常应重启进程:

import importlib
import counter

importlib.reload(counter)

名称空间与名称冲突​

每个模块都有独立的全局名称空间。两个模块可以各自定义同名函数,只要通过模块前缀访问就不会冲突:

import csv_report
import pdf_report

csv_report.export()
pdf_report.export()

如果直接导入同名成员,后一次绑定会覆盖前一次绑定:

from csv_report import export
from pdf_report import export # 当前的 export 已指向 pdf_report.export

此时应保留模块前缀,或使用表达含义的别名:

from csv_report import export as export_csv
from pdf_report import export as export_pdf

还要避免把自己的文件命名为标准库或第三方模块的名称。例如项目中的 json.py、random.py 可能遮蔽真正的标准库模块,导致 import json 导入错误文件。

Python 在哪里查找模块​

可以通过 sys.path 查看模块搜索路径:

import sys

for path in sys.path:
print(path)

搜索路径通常包含:

  • 启动脚本所在目录,或交互式运行时的当前目录;
  • PYTHONPATH 环境变量添加的目录;
  • Python 标准库目录;
  • 当前虚拟环境中的第三方包目录,如 site-packages。

找到第一个匹配项后,Python 通常就停止继续查找,所以同名文件可能遮蔽后面的模块。不要依赖在代码中临时修改 sys.path 来修复项目结构;更可靠的做法是把代码组织成包,并从项目根目录以模块方式运行。

查看一个模块实际来自哪里,可以检查 __file__:

import json

print(json.__file__)

部分内置模块可能没有 __file__。需要在不执行导入的情况下查找模块时,可以使用:

from importlib.util import find_spec

spec = find_spec("json")
print(spec.origin if spec else "未找到")

直接运行与被导入:__name__​

每个模块都有 __name__:

  • 文件被直接运行时,__name__ 的值是 "__main__";
  • 文件被导入时,值通常是它的完整模块名,例如 tools.formatter。

可以用这个差异保留命令行入口,同时避免导入时自动执行:

temperature.py
def celsius_to_fahrenheit(celsius):
return celsius * 9 / 5 + 32


def main():
print(celsius_to_fahrenheit(20))


if __name__ == "__main__":
main()

执行 python temperature.py 会调用 main();执行 import temperature 只会定义函数,不会打印结果。这个判断不是声明模块的必要语法,而是控制“仅直接运行时执行”的常用约定。

模块与包​

单个 .py 文件是模块;包含多个模块的目录通常称为包(package)。例如:

shop/
├── __init__.py
├── pricing.py
└── services/
├── __init__.py
└── order.py

可以使用点号表示层级:

import shop.pricing
from shop.services import order

shop.pricing.calculate_price()
order.create_order()

__init__.py 会在包首次导入时执行,常用于标记普通包和整理包的公开接口。现代 Python 也支持没有 __init__.py 的命名空间包,但一般项目中的普通包保留它会让边界更清晰。

不要在 __init__.py 中放置大量初始化逻辑,否则仅仅导入包就可能产生昂贵操作或复杂的循环依赖。

绝对导入与相对导入​

包内优先使用绝对导入,它从顶层包名开始,来源清晰:

shop/services/order.py
from shop.pricing import calculate_price

相对导入使用前导点表示当前位置,只能在包上下文中使用:

shop/services/order.py
from ..pricing import calculate_price # 上一级包中的 pricing
from .validator import validate_order # 当前包中的 validator
  • . 表示当前包;
  • .. 表示上一级包;
  • 更多点继续向上,但层级过深通常说明模块组织需要调整。

直接执行包内文件,例如 python shop/services/order.py,相对导入可能报错,因为该文件没有正确的包上下文。应在项目根目录使用 -m 按模块运行:

python -m shop.services.order

-m 会按模块名查找并执行代码,使 __package__ 和导入关系保持正确。

从包顶层公开常用名称​

可以在 __init__.py 中重新导出稳定、常用的接口:

shop/__init__.py
from .pricing import calculate_price

__all__ = ["calculate_price"]

使用者就可以写:

from shop import calculate_price

重新导出能简化公共 API,但不要把包内所有名称都搬到顶层,否则会增加名称冲突、导入耗时和循环依赖风险。

标准库、第三方模块与本地模块​

模块按来源通常分为三类:

类型示例获取方式
标准库json、pathlib、datetime随 Python 安装提供
第三方模块requests、pytest在虚拟环境中使用包管理工具安装
本地模块calculator.py、shop.pricing项目自身代码

“安装名”和“导入名”不一定相同。例如安装包可能叫 beautifulsoup4,导入时却写 from bs4 import BeautifulSoup,应以库的官方文档为准。

第三方依赖应安装到项目的虚拟环境,并记录在依赖配置中。不要因为编辑器提示找不到模块,就在代码里硬编码本机的绝对路径。

导入通常按标准库、第三方库、本地代码分组,每组之间空一行:

import json
from pathlib import Path

import requests

from shop.pricing import calculate_price

导入一般放在文件顶部,使依赖一目了然。只有为避免可选依赖、减少确实昂贵的加载,或暂时打破无法立即重构的循环依赖时,才考虑函数内导入,并应说明原因。

动态导入与可选依赖​

模块名只有在运行时才能确定时,可以使用 importlib.import_module():

from importlib import import_module

module_name = "json"
json_module = import_module(module_name)
print(json_module.dumps({"ok": True}))

动态导入适合插件系统,不应替代普通 import。如果模块名来自外部输入,必须使用允许列表限制可导入内容,避免加载任意代码。

可选依赖可以在导入失败时给出明确提示:

try:
import pandas as pd
except ModuleNotFoundError as error:
if error.name != "pandas":
raise
pd = None


def export_excel(data):
if pd is None:
raise RuntimeError("导出 Excel 需要安装 pandas")
# 使用 pd 处理 data

检查 error.name 很重要:pandas 内部缺少其他依赖时,也可能抛出 ModuleNotFoundError;不能把所有导入错误都误判为“没有安装 pandas”。除非确实能处理异常,否则不要使用宽泛的 except ImportError 隐藏问题。

常见问题​

ModuleNotFoundError​

看到 No module named 'xxx' 时,按顺序检查:

  1. 模块名和大小写是否正确;
  2. 当前运行的 Python 是否属于预期虚拟环境;
  3. 第三方依赖是否安装在这个环境中;
  4. 运行命令是否位于正确的项目目录;
  5. 包内代码是否应该使用 python -m package.module 运行;
  6. 本地模块所在目录是否确实位于 sys.path。

可以辅助确认当前解释器:

import sys

print(sys.executable)
print(sys.path)

ImportError: cannot import name ...​

常见原因包括:

  • 名称拼写错误或当前版本没有该 API;
  • 名称没有在目标模块中定义;
  • 模块只执行了一部分就发生异常;
  • 两个模块相互依赖,形成循环导入。

先尝试导入整个模块并查看来源,再检查完整异常堆栈,不要只看最后一行。

循环导入​

假设 users.py 导入 orders.py,而 orders.py 又在顶层导入 users.py。当第二次回到 users.py 时,它仍处于“部分初始化”状态,需要的名称可能还没创建:

users -> orders -> users(尚未完成初始化)

更可靠的解决方式是重新划分依赖:

  • 把双方共用的模型或工具移到第三个模块;
  • 让高层模块依赖底层模块,避免底层反向依赖高层;
  • 将协作对象作为参数传入,而不是在模块顶层互相导入。

函数内延迟导入有时能临时绕开循环,但会隐藏结构问题,不应作为默认方案。

修改导入名称不会修改原模块​

导入只是建立名称绑定:

import math

math_alias = math
math_alias = None

print(math.sqrt(9)) # 3.0,原来的 math 绑定没有改变

不过,通过模块对象修改其属性会影响所有持有同一个模块对象的代码。因此不应随意修改其他模块的全局变量。

实用选择原则​

场景推荐写法
希望来源清晰、避免冲突import module
模块名较长且有明确别名import module as alias
只使用少量、明确的名称from module import name
包内引用稳定的顶层模块优先绝对导入
包内引用相邻实现模块可使用相对导入
按运行时配置加载插件importlib.import_module()
执行包内模块python -m package.module

最后记住三个重点:导入会执行模块顶层代码;成功导入的模块会在当前进程中缓存;清晰的单向依赖比用技巧绕过循环导入更重要。

练习​

创建一个 text_tools 包,包含 formatter.py 和 validator.py:

  1. 在 formatter.py 中编写一个清理首尾空白并统一大小写的函数;
  2. 在 validator.py 中编写一个判断文本是否为空的函数;
  3. 在 text_tools/__init__.py 中公开这两个函数;
  4. 新建 main.py,从包顶层导入并调用它们;
  5. 为 main.py 添加 main() 和 if __name__ == "__main__":;
  6. 分别尝试直接运行脚本和使用 python -m 运行,观察 __name__ 的值。

完成后,尝试故意创建一个名为 json.py 的文件,观察标准库导入如何受到影响,再将它改为不会冲突的名称。