我对“模块”的直觉理解

写代码时,总会把功能拆成多个文件。Python 里,一个以 .py 结尾的文件就是一个“模块”(module)。导入它,就能复用里面的函数、类和变量。模块让代码可分拆、可复用、可维护。

一句话:模块是“带名字的代码文件 + 独立命名空间”。


模块能解决什么问题

  • 复用:写过的函数/类在别处直接 import 使用。
  • 命名隔离:不同文件(模块)有自己的命名空间,减少命名冲突。
  • 组织结构:配合包(package),形成清晰的目录与层次。

模块的本质

  • 一个 .py 文件就是一个模块。
  • 每个模块在加载时会创建一个独立的命名空间,模块名字符串存在 __name__
  • 已加载模块会被缓存到 sys.modules(单例缓存)。再次导入直接复用,避免重复执行。
  • 首次导入时,Python 可能会在 __pycache__/ 写入编译缓存(.pyc)。

最小示例:创建与导入

先写一个文件 fibo.py

# fibo.py

def fib(n: int) -> list[int]:
    a, b = 0, 1
    seq = []
    while a < n:
        seq.append(a)
        a, b = b, a + b
    return seq

print("fibo 模块已加载")  # 导入时会执行顶层代码

在同一目录另写一个脚本:

# main.py
import fibo

print(fibo.fib(20))

运行 python main.py,会看到 fibo 模块已加载 的打印,以及结果序列。

几种常见导入方式:

import fibo                # 推荐:命名空间清晰
import fibo as fb          # 起别名
from fibo import fib       # 直接引入对象名(注意命名污染)
from fibo import fib as f  # 引入并重命名

一般更推荐 import 模块名import 模块名 as 别名,可读性更好且不易冲突。

不推荐使用:

from fibo import *  # 不清晰、易冲突、会覆盖同名标识

作为脚本运行 vs 被导入运行

模块被直接运行时,__name__ == '__main__';被别人导入时,__name__ 等于其模块名。常见写法:

# fibo.py
def fib(n: int) -> list[int]:
    ...

if __name__ == "__main__":
    # 仅当直接执行 fibo.py 时运行
    print(fib(20))

好处:既能当脚本跑自测,也能被安全导入不执行“入口逻辑”。


模块搜索路径(import 到底去哪找)

Python 导入模块时,会按顺序在 sys.path 里这些目录寻找:

  1. 当前脚本所在目录(或当前工作目录)
  2. 标准库路径
  3. 第三方库路径(通常是 site-packages
  4. 环境变量 PYTHONPATH 指定的目录(若有)

可在代码里查看:

import sys
print(sys.path)

临时添加一个搜索路径:

import sys
sys.path.append("/path/to/dir")

更推荐通过项目结构或虚拟环境来管理搜索路径,而不是在代码里动态修改。


“编译”缓存(.pyc 与 pycache

首次导入模块时,Python 会把字节码缓存在 __pycache__/模块名.版本标识.pyc 中,以加速下次启动。这是内部优化,一般无需手动干预。


重新加载模块(开发期热更新)

在交互式环境调试时,改了模块代码,想不重启就生效:

import importlib
import fibo

importlib.reload(fibo)

注意:reload 只在同一解释器进程内有效,且对已从模块“直接导入”的名字无效(例如 from fibo import fibfib 名字不会随 reload 更新)。


包(Packages)与相对导入

当目录里包含多个模块时,可以组织成“包”:

mathkit/
  __init__.py
  stats.py
  series/
    __init__.py
    fibo.py
  • 目录带 __init__.py 即被视为包;内部子目录同理。
  • 导入:
import mathkit.stats
from mathkit import stats
from mathkit.series import fibo

在包内部可以使用相对导入:

# 在 mathkit/series/fibo.py 中
from .. import stats            # 往上一层
from . import helper            # 同一层

更推荐使用“绝对导入”,读起来最直观。相对导入适合包内模块间的本地引用。

补充:现代 Python 支持“命名空间包”(PEP 420),有时即使没有 __init__.py 也能把多个目录视作同一包,常见于复杂项目/安装场景。初学阶段可以先记住“有 __init__.py 最稳妥”。


__all__ 与导出控制

在模块或包的 __init__.py 中定义 __all__ 可控制 from x import * 的导出名单:

# __init__.py
__all__ = ["fib", "stats_mean"]

实际项目里不建议使用 import *,但 __all__ 仍可作为“公开 API 清单”来约定导出的接口。

后续会写一篇博客详细介绍这个 __all__


常见坑与避免方法

  • 同名冲突:自己的模块名不要与标准库/第三方库重名(例如 random.pysys.py)。
  • 工作目录影响导入:在不同目录运行脚本,当前目录会进入 sys.path[0],可能导入到意想不到的模块。
  • 循环导入:模块 A 导入 B,同时 B 又导入 A,容易在导入期就访问到“尚未定义”的对象。解决:
    • 调整依赖结构,抽公共部分到第三个模块;
    • 将导入语句移动到函数内部(延迟导入)。
  • 顶层副作用:模块导入就打印/连接数据库/改全局状态,可能造成隐蔽问题。把“动作”放进 if __name__ == '__main__': 或函数里。

组织项目的简单建议

project/
  pyproject.toml or requirements.txt
  src/
    mypkg/
      __init__.py
      core.py
      utils.py
  tests/
    test_core.py
  • 把代码放在 src/,测试用例单独放 tests/
  • 使用绝对导入from mypkg import core
  • 入口脚本最少逻辑,主要做参数解析与调用库代码。

一个可运行的小例子(包 + 入口)

calc/
  __init__.py
  ops.py
main.py

calc/ops.py

def add(a: float, b: float) -> float:
    return a + b

def sub(a: float, b: float) -> float:
    return a - b

main.py

from calc import ops

def main() -> None:
    print(ops.add(2, 3))
    print(ops.sub(5, 1))

if __name__ == "__main__":
    main()

运行:

python main.py

速查与最佳实践清单

  • 创建模块:写一个 .py 文件。
  • 导入原则:优先 import 包.模块 as 别名;少用 from x import y
  • 入口守卫:脚本入口用 if __name__ == '__main__':
  • 避免循环导入:抽公用、延迟导入、理清依赖方向。
  • 别名与命名:避免与标准库/第三方同名;别名清晰(如 import numpy as np)。
  • 结构化项目:使用包与清晰目录;尽量绝对导入。
  • 调试 reloadimportlib.reload(mod) 仅在交互开发临时使用。

进一步阅读

  • Python 官方教程:Modules(标准模块、搜索路径、包等)
    • https://docs.python.org/3/tutorial/modules.html

这篇笔记到这儿,建议边读边敲,自己建几个小模块试一下,体会“模块 = 文件 + 命名空间”的感觉,基本就通了。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐