前言
大家好,我是 倔强青铜三 。欢迎关注我,微信公众号:倔强青铜三。点赞、收藏、关注,一键三连!
今天我们聊聊**"怎么把一坨一坨的脚本拆成可维护的模块"**。
当你开始写第二个.py文件时,恭喜你,已经迈出了从"脚本小子"到"工程化开发者"的第一步。
**模块(module)和包(package)**是 Python 世界里的第一等公民,搞懂它们,才能让你的代码"写得开、拆得动、跑得稳"。
一口气带你从 "新建一个.py文件" 到 "发布自己的第三方库",全程高能,建议收藏!
🧠 模块到底是什么?
一句话:
模块就是
.py
文件,文件名(去掉.py
)就是模块名。
新建一个 greet.py
,内容如下:
python
# greet.py
def hello(name: str) -> str:
return f"Hello, {name}!"
DEFAULT_NAME = "World"
在当前目录打开解释器:
python
>>> import greet
>>> greet.hello("Pythonista")
'Hello, Pythonista!'
>>> greet.DEFAULT_NAME
'World'
就这么简单!
import 语句 会把文件从头到尾执行一遍,并把产生的全局命名空间 打包成一个模块对象 ,挂到当前命名空间下。
注意:
- 模块对象的名字就是文件名(
greet
)。 - 模块里定义的变量、函数、类,都变成模块对象的属性。
🔁 模块只加载一次!
Python 为了效率,解释器会话里同名模块只会加载一次 。
你可以验证:
python
>>> import greet
>>> import greet # 再导入一次
>>> greet.__file__ # 看看文件路径
'/Users/bronze/greet.py'
修改了 greet.py
的内容?
重启解释器,或者使用 importlib.reload
:
python
>>> import importlib, greet
>>> importlib.reload(greet)
<module 'greet'>
📦 四种 import 姿势
姿势 | 示例 | 特点 |
---|---|---|
基础导入 | import greet |
用 greet.xxx 访问,最简洁,不会污染当前命名空间 |
起别名 | import greet as gr |
名字太长就给它起外号 |
精确导入 | from greet import hello |
把 hello 直接放进当前命名空间,慎用同名覆盖 |
全部导入 | from greet import * |
一股脑儿全搬进来,不推荐,会踩命名冲突的坑 |
🏗️ 把模块当脚本跑:__main__
魔法
很多时候,我们既希望一个文件"能被 import",又希望"能双击直接跑"。
在文件底部加上:
python
if __name__ == "__main__":
import sys
name = sys.argv[1] if len(sys.argv) > 1 else DEFAULT_NAME
print(hello(name))
终端里:
bash
$ python greet.py Alice
Hello, Alice!
解释器 import 时,__name__
等于模块名;当脚本执行时,__name__
等于 "__main__"
,所以这段代码只在脚本模式下跑。
🗂️ 项目大了怎么办?------包(package)登场!
想象你写了 N 个模块:
markdown
sound/ 顶层包
├── __init__.py 必须有,哪怕是空文件
├── formats/ 子包
│ ├── __init__.py
│ ├── wavread.py
│ └── wavwrite.py
├── effects/ 子包
│ ├── __init__.py
│ ├── echo.py
│ └── surround.py
└── filters/
├── __init__.py
└── equalizer.py
导入方式一览:
python
import sound.effects.echo # 绝对导入
from sound.effects import echo # 子包导入
from sound.effects.echo import echofilter # 精确导入
from . import echo # 相对导入(只能在包里用)
🛠️ 模块搜索路径:Python 怎么找到文件?
import 时,解释器按顺序去下面几个地方找:
- 内置模块 (
sys.builtin_module_names
)。 sys.path
列表,初始化来源:- 当前脚本所在目录(或交互模式下的当前目录)。
- 环境变量
PYTHONPATH
指定的目录。 - 标准库目录 &
site-packages
(pip 装的三方库就在这里)。
python
>>> import sys, pprint
>>> pprint.pp(sys.path)
['',
'/usr/local/lib/python3.13',
'/usr/local/lib/python3.13/site-packages',
...]
想把自己的目录塞进去?运行时修改即可:
python
import sys
sys.path.insert(0, '/Users/bronze/my_libs')
🧪 实战:写一个带 CLI 的实用模块
需求:写一个数学工具包 mymath
,支持求阶乘、斐波那契,还能在命令行里用。
目录结构:
markdown
mymath_proj/
├── mymath/
│ ├── __init__.py
│ ├── factorial.py
│ └── fibonacci.py
└── demo.py
1. factorial.py
python
def fact(n: int) -> int:
"""递归阶乘"""
if n < 0:
raise ValueError("n must be non-negative")
return 1 if n in (0, 1) else n * fact(n - 1)
2. fibonacci.py
python
def fib(n: int) -> list[int]:
"""返回 <= n 的斐波那契数列"""
a, b = 0, 1
res = []
while a <= n:
res.append(a)
a, b = b, a + b
return res
3. init.py 统一导出
python
from .factorial import fact
from .fibonacci import fib
__all__ = ["fact", "fib"]
__version__ = "1.0.0"
4. demo.py 脚本入口
python
#!/usr/bin/env python3
import argparse, sys, os
# 把项目根目录塞进 sys.path,确保能 import mymath
sys.path.insert(0, os.path.join(os.path.dirname(__file__)))
from mymath import fact, fib
def main():
parser = argparse.ArgumentParser(description="CLI for mymath")
sub = parser.add_subparsers(dest="cmd")
sub.add_parser("fact").add_argument("n", type=int)
sub.add_parser("fib").add_argument("n", type=int)
args = parser.parse_args()
match args.cmd:
case "fact":
print(f"{args.n}! =", fact(args.n))
case "fib":
print("fib <=", args.n, "->", fib(args.n))
case _:
parser.print_help()
if __name__ == "__main__":
main()
跑一下:
bash
$ python demo.py fact 5
5! = 120
$ python demo.py fib 20
fib <= 20 -> [0, 1, 1, 2, 3, 5, 8, 13]
📦 打包发布:让全世界 pip install 你的库!
1. 创建 pyproject.toml(PEP 621 标准)
toml
[build-system]
requires = ["setuptools>=61", "wheel"]
build-backend = "setuptools.build_meta"
[project]
name = "mymath-bronze"
version = "1.0.0"
description = "A tiny math toolkit"
authors = [{name="倔强青铜三", email="bronze@example.com"}]
readme = "README.md"
requires-python = ">=3.10"
dependencies = []
[project.scripts]
mymath-cli = "mymath.__main__:main"
2. 新增 main.py,让包自己可执行
python
# mymath/__main__.py
from ..demo import main
if __name__ == "__main__":
main()
3. 构建 & 发布
bash
python -m pip install build twine
python -m build # 生成 dist/*.whl & *.tar.gz
python -m twine upload dist/* # 需要 PyPI 账号
别人就能:
bash
pip install mymath-bronze
python -m mymath fib 100
🧾 速查表:30 秒回顾
场景 | 代码片段 |
---|---|
导入模块 | import mod |
起别名 | import numpy as np |
精确导入 | from pathlib import Path |
相对导入 | from .utils import helper |
查看已加载模块 | sys.modules |
查看搜索路径 | sys.path |
重新加载 | importlib.reload(mod) |
脚本/模块双用 | if __name__ == "__main__" |
定义包 | 目录 + __init__.py |
控制 from pkg import * |
__all__ = ["foo", "bar"] |
命令行入口 | __main__.py 或 project.scripts |
🚑 常见坑 & 调试技巧
- ImportError / ModuleNotFoundError
- 检查文件名、目录、虚拟环境、
sys.path
。
- 检查文件名、目录、虚拟环境、
- 循环导入
- A 导入 B,B 又导入 A → 拆分公共部分到第三个模块。
- 相对导入报错
- 确保包目录顶层在
sys.path
,或python -m pkg.module
运行。
- 确保包目录顶层在
- IDE 提示找不到
- 把项目根标记为 Sources Root;或配置
PYTHONPATH
。
- 把项目根标记为 Sources Root;或配置
✅ 一句话总结
模块让你把代码拆成可复用的小积木;包让你把模块再按层级组织;
掌握 import
的四种姿势、__main__
的双模式、sys.path
的搜索顺序,
你就能把任何脚本升级成可维护、可分发、可 pip install 的专业级项目!
最后感谢阅读!欢迎关注我,微信公众号:
倔强青铜三
。欢迎点赞
、收藏
、关注
,一键三连!!!