github star gitee star atomgit star PyPI Downloads AI 编程 AI 交流群

大家好,我是正在实战各种AI项目的程序员晚枫。

今天聊一个让新手困惑、老手也容易忽视的话题——Python的模块与包


从一个真实的代码灾难说起

去年有个学员发给我一个项目,问为什么跑不起来。我看了一下目录结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
project/
├── test.py
├── utils.py
├── utils.py.bak
├── new_utils.py
├── old_utils.py
├── 处理数据.py
├── 数据分析_v2.py
├── 数据分析_v3_final.py
├── 数据分析_v3_final_最终版.py
├── main.py
├── main_backup.py
└── 配置文件.txt

问题

  • 文件命名混乱
  • 功能分散在多个文件
  • 不知道该运行哪个文件
  • 有循环导入错误

他可能写过很多Python文件,但当项目变大时,代码变得混乱不堪:函数找不到定义、循环导入报错、同名文件冲突...

这篇文章总结了我在项目实战中总结的5个组织原则,帮你写出井井有条的Python代码。


概念1:模块就是.py文件

什么是模块?

任何一个.py文件都是一个模块。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# math_utils.py(这是一个模块)
"""数学工具模块"""

PI = 3.14159

def add(a, b):
"""加法"""
return a + b

def multiply(a, b):
"""乘法"""
return a * b

class Calculator:
"""计算器类"""
def __init__(self):
self.result = 0

def add(self, num):
self.result += num
return self.result

导入模块的多种方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
# 方式1:import整个模块
import math_utils

print(math_utils.add(2, 3)) # 5
print(math_utils.PI) # 3.14159
calc = math_utils.Calculator()

# 方式2:from import特定内容
from math_utils import add, PI

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

# 方式3:import as起别名
import math_utils as mu

print(mu.add(2, 3)) # 5

# 方式4:from import as
from math_utils import add as add_numbers

print(add_numbers(2, 3)) # 5

# 方式5:导入所有内容(不推荐)
from math_utils import *

print(add(2, 3))
print(PI)
# 问题:不知道变量来自哪里,容易命名冲突

模块搜索路径

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import sys

# Python会在这些路径中搜索模块
for path in sys.path:
print(path)

# 输出:
# /Users/wanfeng/projects/my_project(当前目录)
# /usr/local/lib/python3.11/site-packages(第三方包)
# /usr/local/lib/python3.11(标准库)
# ...

# 添加搜索路径
sys.path.append('/custom/module/path')

模块的__name__属性

1
2
3
4
5
6
7
8
9
10
11
12
# my_module.py

def main():
print("主程序执行")

# 只有直接运行这个文件时才执行
if __name__ == "__main__":
main()

# 导入时不执行
# import my_module # 不会执行main()
# python my_module.py # 会执行main()

概念2:包是模块的集合

什么是包?

包含__init__.py文件的文件夹就是一个包。

1
2
3
4
5
6
my_project/
├── main.py
└── my_package/ # 这是一个包
├── __init__.py # 包的初始化文件(可以为空)
├── module_a.py # 模块A
└── module_b.py # 模块B

导入包的方式

1
2
3
4
5
6
7
8
9
10
11
12
13
# 导入包中的模块
import my_package.module_a
my_package.module_a.func()

from my_package import module_a
module_a.func()

from my_package.module_a import func
func()

# 导入包本身
import my_package
# 会执行my_package/__init__.py

包的层级结构

1
2
3
4
5
6
7
8
9
10
my_project/
├── main.py
└── my_package/ # 顶级包
├── __init__.py
├── module_a.py
├── module_b.py
└── subpackage/ # 子包
├── __init__.py
├── module_c.py
└── module_d.py
1
2
3
4
5
# 导入子包
from my_package.subpackage import module_c

# 或者
import my_package.subpackage.module_c as mc

Python 3.3+ 的命名空间包

1
2
3
4
5
6
7
8
9
10
# Python 3.3+ 允许没有__init__.py的包(命名空间包)
# 可以跨多个目录

# path1/mypackage/module_a.py
# path2/mypackage/module_b.py

# sys.path包含path1和path2时
import mypackage.module_a
import mypackage.module_b
# 都可以正常导入

原则1:合理划分模块

按功能划分

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
project/
├── main.py # 程序入口
├── config.py # 配置文件
├── utils/ # 工具函数
│ ├── __init__.py
│ ├── file_utils.py # 文件操作
│ ├── date_utils.py # 日期处理
│ └── string_utils.py # 字符串处理
├── models/ # 数据模型
│ ├── __init__.py
│ ├── user.py
│ └── order.py
├── services/ # 业务逻辑
│ ├── __init__.py
│ ├── user_service.py
│ └── order_service.py
└── tests/ # 测试代码
├── __init__.py
├── test_user.py
└── test_order.py

每个模块的职责要单一

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# ✅ 好的实践:职责清晰
# file_utils.py
"""文件操作工具"""

def read_file(path):
"""读取文件"""
with open(path, 'r', encoding='utf-8') as f:
return f.read()

def write_file(path, content):
"""写入文件"""
with open(path, 'w', encoding='utf-8') as f:
f.write(content)

# ❌ 坏的实践:职责混杂
# utils.py
def read_file(path): # 文件操作
pass

def send_email(to): # 邮件发送
pass

def calculate_tax(money): # 税务计算
pass

模块命名规范

1
2
3
4
5
6
7
8
9
10
# ✅ 好的命名
user_service.py # 小写,下划线分隔
file_utils.py # 清晰表达功能
data_processor.py # 一眼就知道用途

# ❌ 坏的命名
utils.py # 太笼统
my_module.py # 没有信息量
test.py # 容易和测试混淆
处理数据.py # 不要用中文

原则2:避免循环导入

什么是循环导入?

1
2
3
4
5
6
7
8
9
10
11
# a.py
from b import func_b

def func_a():
return func_b()

# b.py
from a import func_a # 循环导入!

def func_b():
return "hello"

运行python a.py会报错:

1
ImportError: cannot import name 'func_b' from partially initialized module 'b'

解决方案

方案1:合并模块

1
2
3
4
5
6
# common.py
def func_a():
return func_b()

def func_b():
return "hello"

方案2:延迟导入(推荐)

1
2
3
4
5
6
7
8
# a.py
def func_a():
from b import func_b # 在函数内部导入
return func_b()

# b.py
def func_b():
return "hello"

方案3:只导入模块,不导入内容

1
2
3
4
5
6
7
8
9
10
11
# a.py
import b # 只导入模块

def func_a():
return b.func_b() # 使用时再引用

# b.py
import a

def func_b():
return "hello"

方案4:重构设计(最佳)

1
2
3
4
5
6
7
8
9
# c.py - 存放共享的内容
def shared_function():
pass

# a.py
from c import shared_function

# b.py
from c import shared_function

常见循环导入场景

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# 场景1:模型之间的关联
# models/user.py
from models.order import Order

class User:
def get_orders(self):
return Order.query.filter_by(user_id=self.id)

# models/order.py
from models.user import User # 循环导入!

class Order:
def get_user(self):
return User.query.get(self.user_id)

# 解决:使用TYPE_CHECKING
# models/user.py
from typing import TYPE_CHECKING

if TYPE_CHECKING:
from models.order import Order

class User:
def get_orders(self) -> list['Order']:
from models.order import Order
return Order.query.filter_by(user_id=self.id)

原则3:使用相对导入

绝对导入 vs 相对导入

1
2
3
4
5
6
7
8
9
10
# 项目结构
project/
├── main.py
└── my_package/
├── __init__.py
├── module_a.py
├── module_b.py
└── subpackage/
├── __init__.py
└── module_c.py
1
2
3
4
5
6
7
8
9
# 绝对导入(从项目根目录开始)
from my_package import module_a
from my_package.subpackage import module_c

# 相对导入(相对于当前位置)
# 在module_b.py中
from . import module_a # 同级目录
from .subpackage import module_c # 子目录
from .. import module_a # 上级目录(如果在subpackage中)

相对导入语法

1
2
3
4
from . import module          # 当前目录
from .. import module # 上级目录
from ... import module # 上上级目录
from .subpackage import module # 子目录

什么时候用相对导入?

优点

  • 包内部的模块之间互相引用
  • 移动包的位置时不需要修改导入语句

缺点

  • 只能在包内部使用
  • 运行单个文件时可能报错
1
2
3
4
5
6
7
8
9
10
11
# 推荐:包内部用相对导入
# my_package/module_b.py
from . import module_a

# 推荐:包外部用绝对导入
# main.py
from my_package import module_a

# ❌ 不要混用
# my_package/module_b.py
from my_package import module_a # 如果改包名要改这里

运行相对导入的文件

1
2
3
4
5
6
7
8
9
10
11
# 问题:直接运行包内的文件会报错
# python my_package/module_a.py
# ImportError: attempted relative import with no known parent package

# 解决方案1:以模块方式运行
# python -m my_package.module_a

# 解决方案2:添加路径
import sys
from pathlib import Path
sys.path.append(str(Path(__file__).parent.parent))

原则4:善用init.py

init.py的作用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# my_package/__init__.py

# 1. 标识这是一个包(可以为空)

# 2. 控制对外暴露的内容
from .module_a import func_a, ClassA
from .module_b import func_b

__all__ = ['func_a', 'ClassA', 'func_b']

# 3. 执行初始化代码
print("my_package 已加载")

# 4. 设置包级变量
VERSION = "1.0.0"

# 5. 简化导入路径
# 用户可以直接:from my_package import func_a
# 而不需要:from my_package.module_a import func_a

实战案例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# my_package/__init__.py

# 按需导入(避免导入所有内容)
def __getattr__(name):
"""延迟导入"""
if name == 'heavy_module':
from . import heavy_module
return heavy_module
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")

# 导入常用内容
from .core import main_function

# 版本信息
__version__ = "1.0.0"
__author__ = "程序员晚枫"

# 公开API
__all__ = ['main_function', '__version__']

包级别的配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# my_package/__init__.py
import logging

# 配置包的日志
logger = logging.getLogger(__name__)
logger.setLevel(logging.INFO)

# 包级别的配置
DEBUG = False

def enable_debug():
global DEBUG
DEBUG = True
logger.setLevel(logging.DEBUG)

原则5:管理第三方依赖

requirements.txt

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# requirements.txt

# 精确版本
requests==2.31.0

# 最小版本
pandas>=1.5.0

# 版本范围
numpy>=1.20.0,<2.0.0

# 只指定包名(最新版本)
flask

# 开发依赖(注释标注)
pytest # testing
black # formatting

生成和使用

1
2
3
4
5
6
7
8
# 生成依赖清单
pip freeze > requirements.txt

# 安装依赖
pip install -r requirements.txt

# 只安装生产依赖
pip install -r requirements.txt --only-binary :all:

使用pip-tools

1
2
3
4
5
6
7
8
9
10
11
12
13
# 安装
pip install pip-tools

# 创建requirements.in
# requirements.in
requests
pandas
numpy

# 编译生成requirements.txt
pip-compile requirements.in

# 会自动解析依赖关系,锁定版本

setup.py / pyproject.toml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# setup.py(传统方式)
from setuptools import setup, find_packages

setup(
name='my_package',
version='0.1.0',
packages=find_packages(),
install_requires=[
'requests>=2.25.0',
'pandas>=1.5.0',
],
extras_require={
'dev': [
'pytest>=7.0.0',
'black>=22.0.0',
]
},
entry_points={
'console_scripts': [
'my_cli=my_package.cli:main',
],
},
)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# pyproject.toml(现代方式,PEP 621)
[project]
name = "my_package"
version = "0.1.0"
description = "My awesome package"
authors = [{name = "程序员晚枫", email = "wanfeng@example.com"}]

dependencies = [
"requests>=2.25.0",
"pandas>=1.5.0",
]

[project.optional-dependencies]
dev = [
"pytest>=7.0.0",
"black>=22.0.0",
]

[project.scripts]
my_cli = "my_package.cli:main"

虚拟环境(必学)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 使用venv(Python内置)
python -m venv venv

# 激活(Windows)
venv\Scripts\activate

# 激活(Mac/Linux)
source venv/bin/activate

# 退出
deactivate

# 使用virtualenv
pip install virtualenv
virtualenv venv

# 使用poetry(现代工具)
pip install poetry
poetry new my_project
poetry install

实战:创建一个可发布的包

项目结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
my_tool/                      # 项目根目录
├── README.md # 项目说明
├── LICENSE # 许可证
├── pyproject.toml # 项目配置
├── setup.py # 安装配置(可选)
├── requirements.txt # 依赖
├── tests/ # 测试
│ ├── __init__.py
│ └── test_core.py
└── my_tool/ # 包目录
├── __init__.py
├── core.py # 核心功能
├── cli.py # 命令行接口
└── utils.py # 工具函数

代码实现

1
2
3
4
5
6
7
8
9
# my_tool/__init__.py
"""My Tool - 一个实用的工具包"""

__version__ = "0.1.0"
__author__ = "程序员晚枫"

from .core import main_function

__all__ = ['main_function', '__version__']
1
2
3
4
5
6
7
8
9
10
# my_tool/core.py
"""核心功能"""

def main_function():
"""主函数"""
return "Hello from my_tool!"

def helper_function():
"""辅助函数"""
return "This is a helper"
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# my_tool/cli.py
"""命令行接口"""

import argparse
from .core import main_function

def main():
"""命令行入口"""
parser = argparse.ArgumentParser(description='My Tool')
parser.add_argument('--version', action='version', version='%(prog)s 0.1.0')
parser.add_argument('command', help='Command to run')

args = parser.parse_args()

if args.command == 'hello':
print(main_function())

if __name__ == '__main__':
main()
1
2
3
4
5
6
# my_tool/utils.py
"""工具函数"""

def format_output(data):
"""格式化输出"""
return f"Result: {data}"

配置文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
# pyproject.toml
[build-system]
requires = ["setuptools>=61.0"]
build-backend = "setuptools.build_meta"

[project]
name = "my_tool"
version = "0.1.0"
description = "A useful tool package"
readme = "README.md"
requires-python = ">=3.8"
authors = [
{name = "程序员晚枫", email = "wanfeng@example.com"}
]
classifiers = [
"Programming Language :: Python :: 3",
"License :: OSI Approved :: MIT License",
]

dependencies = [
"requests>=2.25.0",
]

[project.optional-dependencies]
dev = [
"pytest>=7.0.0",
"black>=22.0.0",
]

[project.scripts]
my_tool = "my_tool.cli:main"

[project.urls]
Homepage = "https://github.com/CoderWanFeng/my_tool"

发布到PyPI

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 安装构建工具
pip install build twine

# 构建
python -m build

# 上传到TestPyPI(测试)
twine upload --repository testpypi dist/*

# 上传到PyPI
twine upload dist/*

# 安装
pip install my_tool

高级技巧

动态导入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import importlib

# 动态导入模块
module = importlib.import_module('my_package.module_a')

# 使用模块
result = module.some_function()

# 动态导入并调用函数
func = getattr(module, 'some_function')
result = func()

# 重新加载模块
importlib.reload(module)

插件系统

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
# plugins/__init__.py
import importlib
from pathlib import Path

def load_plugins(plugin_dir='plugins'):
"""加载插件"""
plugins = {}
plugin_path = Path(plugin_dir)

for file in plugin_path.glob('*.py'):
if file.name.startswith('_'):
continue

module_name = file.stem
module = importlib.import_module(f'{plugin_dir}.{module_name}')

if hasattr(module, 'register'):
plugins[module_name] = module.register()

return plugins

# plugins/hello.py
def register():
return {
'name': 'Hello Plugin',
'version': '1.0.0',
'run': lambda: print("Hello from plugin!")
}

# 使用
plugins = load_plugins()
plugins['hello']['run']()

懒加载

1
2
3
4
5
6
7
8
9
10
11
12
13
# my_package/__init__.py
def __getattr__(name):
"""延迟导入,减少启动时间"""
if name == 'heavy_module':
from . import heavy_module
return heavy_module
elif name == 'expensive_class':
from .expensive_module import ExpensiveClass
return ExpensiveClass
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")

# 使用时才导入
from my_package import heavy_module # 此时才导入

避坑指南

坑1:导入所有内容

1
2
3
4
5
6
7
8
9
# ❌ 不推荐
from module import *

# 不知道导入了什么
# 容易命名冲突
# 代码难以理解

# ✅ 推荐
from module import func_a, func_b

坑2:模块名冲突

1
2
3
4
5
6
7
8
# 创建了一个叫json.py的文件
# json.py
def my_function():
pass

# main.py
import json # 导入的是你的文件,不是标准库!
json.dumps({}) # AttributeError

解决:避免使用标准库名作为模块名

坑3:相对导入在脚本中失败

1
2
3
4
5
6
7
# 直接运行包内的文件
# python my_package/module_a.py

# 错误:ImportError: attempted relative import

# 解决:以模块方式运行
# python -m my_package.module_a

坑4:忘记init.py

1
2
3
4
5
6
7
# Python 2 和 Python 3.3 之前
# 没有__init__.py的文件夹不是包

# Python 3.3+
# 支持命名空间包,但没有__init__.py可能导致问题

# 建议:始终添加__init__.py

推荐:AI Python零基础实战营

想系统学习Python工程化开发?

课程内容:

  • ✅ Python基础语法
  • ✅ 模块与包管理
  • ✅ 项目结构设计
  • ✅ 实战项目练习

🎁 限时福利:送《Python编程从入门到实践》实体书

👉 点击了解详情


相关阅读


PS:好的代码组织能让项目更易维护。记住:高内聚、低耦合、职责单一、避免循环导入。


📚 推荐教材

主教材《Python 编程从入门到实践(第 3 版)》


📚 推荐:Python 零基础实战营

系统学习Python,推荐这个免费入门课程 👇

特点说明
🎯 专为0基础设计门槛低,上手快
📹 配套视频讲解配合文章学习效果更好
💬 专属答疑群遇到问题有人带
🎁 实体书赠送优秀学员送《Python编程从入门到实践》

👉 点击免费领取 Python 零基础实战营

💬 联系我

平台账号/链接
微信扫码加好友
微博@程序员晚枫
知乎@程序员晚枫
抖音@程序员晚枫
小红书@程序员晚枫
B 站Python 自动化办公社区

主营业务:AI 编程培训、企业内训、技术咨询

🎓 AI 编程实战课程

想系统学习 AI 编程?程序员晚枫的 AI 编程实战课 帮你从零上手!