RSS
菜单
全部文章快讯开发科技深度热点

模块与包(Python 从精通到入门 · 19)

内容摘要

一个 .py 文件就是一个模块,一个目录(含 init.py)就是一个包。这篇讲清楚代码怎么拆、怎么导入、怎么组织成一个正经项目。

一个 .py 文件就是一个模块,一个目录(含 __init__.py)就是一个包。这篇讲清楚代码怎么拆、怎么导入、怎么组织成一个正经项目。

你将学到

  • import 背后的机制,以及 __name__ == "__main__" 到底在防什么
  • 模块搜索路径 sys.path 是怎么拼出来的,导入失败怎么排查
  • 包 = 目录 + __init__.py,绝对导入与相对导入怎么选
  • __all__ 如何定义模块的公开接口
  • 常见标准库速览,以及一个多文件小项目的组织方式

前置知识

上一篇《面向对象编程》我们写了不少类。这一篇告诉你:这些类该放哪个文件里、怎么被别的文件用起来。

模块就是一个 .py 文件

任何 .py 文件都是模块。别人 import 你的文件,就能用你定义的函数、类和变量。

# 文件: greet.py
def hello(name):
    return f"你好,{name}"

PI = 3.14
# 文件: main.py (和 greet.py 同目录)
import greet

print(greet.hello("小明"))   # 输出: 你好,小明
print(greet.PI)              # 输出: 3.14

import greet 会把 greet.py 整个执行一遍,然后把里面的名字挂到 greet 这个命名空间下。

import 的几种写法

import greet                     # 用 greet.xxx 访问,推荐,来源清晰
import greet as g                # 起别名,减少打字
from greet import hello          # 只导入一个名字,之后直接 hello(...)
from greet import hello, PI      # 导入多个
from greet import *              # ❌ 导入全部,污染命名空间,别用

选择建议:优先 import 模块,来源一目了然;名字太长再 as 起别名;from ... import 只在名字少且明确时用。

__name__ 与 __main__

每个模块都有一个内置变量 __name__。直接运行这个文件时,它的值是 "__main__";被别人 import 时,值是模块名。

# 文件: greet.py
def hello(name):
    return f"你好,{name}"

if __name__ == "__main__":       # 只有直接运行本文件时才执行
    print(hello("世界"))          # 输出: 你好,世界

这行的作用是:让文件既能当库被导入、又能当脚本直接跑。如果你不加这个判断,别人 import 你时,测试代码会莫名其妙执行。

# python greet.py         -> 打印"你好,世界"(走 __main__ 分支)
# python -c "import greet" -> 什么都不打印(__name__ 是 "greet")

模块搜索路径 sys.path

import greet 时,Python 按这个顺序找文件:

  1. 内置模块(sys、math 等,编译在解释器里)。
  2. sys.path 列表里的目录,依次查找。列表通常包含: - 脚本所在目录(或空字符串,代表当前目录) - PYTHONPATH 环境变量里的目录 - 安装的第三方包目录(site-packages)
import sys
print(sys.path[:3])            # 输出: 前几项,通常含脚本目录与 site-packages

排查 ModuleNotFoundError 的第一招,就是把 sys.path 打出来,看你要导入的文件到底在不在里面。临时加路径:

import sys
sys.path.append("/path/to/my/lib")   # 临时把某目录加进搜索路径
import mylib                          # 现在能找到了

不过更正规的方式是用包结构,或者正式安装(pip install -e .),别到处 sys.path.append。

包 = 目录 + __init__.py

包是"装着多个模块的目录"。传统上要求目录里有 __init__.py 才算包。

myapp/
├── __init__.py        # 让 myapp 成为一个包
├── text.py
└── mathx.py

__init__.py 可以是空文件,也可以在里面对外"导出"一些名字,方便使用:

# 文件: myapp/__init__.py
from .text import clean        # 把子模块的名字提到包的顶层

这样别人就能 from myapp import clean,而不用写 from myapp.text import clean。

注:Python 3.3+ 有"命名空间包",没有 __init__.py 的目录也可能被当包。但为了明确和兼容,自己写包时仍然加上 __init__.py。

绝对导入与相对导入

  • 绝对导入:从包的顶层写起,最清楚。
  • 相对导入:用点号表示"当前/上级",. 当前包,.. 上一级。
# 文件: myapp/mathx.py

# 绝对导入
from myapp.text import clean

# 相对导入(同一个包内更省事)
from .text import clean         # 同目录下的 text
from . import text              # 导入同级模块本身

要点:相对导入只在包里有效,直接运行的顶层脚本不能用 from . import x,否则报 ImportError: attempted relative import with no known parent package。

日常建议:包内文件之间用相对导入,包外引用用绝对导入。

__all__:定义公开接口

__all__ 是一个字符串列表,决定 from module import * 时导出哪些名字。

# 文件: greet.py
__all__ = ["hello"]            # 只导出 hello

def hello(name):
    return f"你好,{name}"

def _internal():               # 下划线开头,本就不打算公开
    return "内部用"

from greet import * 之后,只有 hello 可用。这既是文档(告诉别人哪些是公开接口),也是约束。

常用标准库速览

标准库随 Python 一起装,不用 pip。先认识几个最常用的。

import sys, os, math, random, datetime
from collections import Counter, defaultdict
import itertools, functools

# sys:解释器相关
print(sys.version_info.major)                  # 输出: 3
print(sys.argv)                                # 输出: 命令行参数列表

# os:操作系统接口(路径推荐用更现代的 pathlib)
print(os.getcwd())                             # 输出: 当前工作目录
print(os.path.join("a", "b"))                  # 输出: a/b(Windows 上是 a\b)

# math:数学
print(math.sqrt(16))                           # 输出: 4.0
print(math.gcd(12, 18))                        # 输出: 6

# random:随机
print(random.randint(1, 6))                    # 输出: 1~6 之间的整数
print(random.choice(["石头", "剪刀", "布"]))    # 输出: 随机一项

# datetime:日期时间
now = datetime.datetime.now()
print(now.year)                                # 输出: 当前年份
print(datetime.date(2026, 10, 7).isoformat())  # 输出: 2026-10-07
# collections:更好用的容器
c = Counter("abracadabra")
print(c["a"])                                  # 输出: 5
d = defaultdict(list)
d["fruits"].append("苹果")                     # 不会 KeyError
print(d["fruits"])                             # 输出: ['苹果']

# itertools:迭代器工具箱
print(list(itertools.combinations([1, 2, 3], 2)))
# 输出: [(1, 2), (1, 3), (2, 3)]

# functools:函数工具
print(functools.reduce(lambda a, b: a + b, [1, 2, 3, 4]))   # 输出: 10

这些都是"内置武器库",下一篇《推导式与函数式工具》会深入 itertools 和 functools。

拆成多文件的小项目

一个像样的项目,通常长这样:

todo/
├── main.py              # 程序入口,只有一小段启动代码
├── todo/
│   ├── __init__.py
│   ├── storage.py       # 负责读写数据
│   └── models.py        # 定义数据结构(类)
└── tests/
    └── test_storage.py
# 文件: todo/models.py
class Task:
    def __init__(self, title, done=False):
        self.title = title
        self.done = done
# 文件: todo/storage.py
import json
from .models import Task                 # 包内相对导入

def save(tasks, path):
    with open(path, "w", encoding="utf-8") as f:
        json.dump([t.__dict__ for t in tasks], f, ensure_ascii=False)
# 文件: main.py
from todo.models import Task
from todo.storage import save

if __name__ == "__main__":               # 入口脚本的标准写法
    save([Task("学 Python")], "tasks.json")
    print("已保存")                       # 输出: 已保存

拆分的经验法则:入口文件负责启动,功能按职责分模块,每个模块只做一件事。

常见坑

❌ 文件名和标准库同名,导致导入错对象。

# 目录里有 json.py,然后:
import json          # ❌ 导入的是你自己的 json.py,不是标准库

✅ 换个名字,别用 json.py/random.py/sys.py 这类名字。

❌ 直接运行的脚本里用相对导入。

# 直接运行 python script.py,脚本里有:
from .utils import helper      # ❌ ImportError: no known parent package

✅ 顶层脚本用绝对导入。

from utils import helper       # ✅ 前提是 utils 在搜索路径里

❌ 模块级代码写太多"有副作用"的语句。

# config.py
print("加载配置...")            # ❌ 一旦被 import 就打印
conn = connect_db()            # ❌ import 时就连数据库,很危险

✅ 包进函数,只在需要时调用。

def init():
    print("加载配置...")
    return connect_db()

小结

  • 一个 .py 文件就是模块;目录 + __init__.py 就是包。
  • if __name__ == "__main__": 让文件既能当库、又能当脚本。
  • 导入靠 sys.path 搜索,找不到时先打印 sys.path 排查。
  • 包内用相对导入(./..),包外用绝对导入;别用 from x import *。
  • 用 __all__ 声明公开接口;_name 表示内部使用。
  • 项目结构:入口精简,功能按职责拆模块,别让 import 产生副作用。

延伸阅读

  • 熟练篇《文件与路径处理》讲了 pathlib,比 os.path 更现代,路径操作优先用它。
  • 进阶篇《包管理与虚拟环境》会讲 pip、venv 和 pyproject.toml,把项目真正"打包"起来。

上一篇:面向对象编程 · 下一篇:异常处理

— 全文完 —回到顶部 ↑
下载推广海报

文章推广海报

《模块与包(Python 从精通到入门 · 19)》完整推广海报
DISCUSSION

文章回复

0 条公开回复
未登录回复需要审核后公开
还没有回复,欢迎参与讨论。