一个
.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 按这个顺序找文件:
- 内置模块(
sys、math等,编译在解释器里)。 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,把项目真正"打包"起来。
上一篇:面向对象编程 · 下一篇:异常处理
文章回复
0 条公开回复