大家好,我是正在实战各种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 """数学工具模块""" 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 import math_utilsprint (math_utils.add(2 , 3 )) print (math_utils.PI) calc = math_utils.Calculator() from math_utils import add, PIprint (add(2 , 3 )) print (PI) import math_utils as muprint (mu.add(2 , 3 )) from math_utils import add as add_numbersprint (add_numbers(2 , 3 )) 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 sysfor path in sys.path: print (path) sys.path.append('/custom/module/path' )
模块的__name__属性 1 2 3 4 5 6 7 8 9 10 11 12 def main (): print ("主程序执行" ) if __name__ == "__main__" : 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_amy_package.module_a.func() from my_package import module_amodule_a.func() from my_package.module_a import funcfunc() import my_package
包的层级结构 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_cimport my_package.subpackage.module_c as mc
Python 3.3+ 的命名空间包 1 2 3 4 5 6 7 8 9 10 import mypackage.module_aimport 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 """文件操作工具""" 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) 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 from b import func_bdef func_a (): return func_b() 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 def func_a (): return func_b() def func_b (): return "hello"
方案2:延迟导入(推荐) 1 2 3 4 5 6 7 8 def func_a (): from b import func_b return func_b() def func_b (): return "hello"
方案3:只导入模块,不导入内容 1 2 3 4 5 6 7 8 9 10 11 import b def func_a (): return b.func_b() import adef func_b (): return "hello"
方案4:重构设计(最佳) 1 2 3 4 5 6 7 8 9 def shared_function (): pass from c import shared_functionfrom 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 from models.order import Orderclass User : def get_orders (self ): return Order.query.filter_by(user_id=self.id ) from models.user import User class Order : def get_user (self ): return User.query.get(self.user_id) from typing import TYPE_CHECKINGif 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_afrom my_package.subpackage import module_cfrom . import module_a from .subpackage import module_c from .. import module_a
相对导入语法 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 from . import module_afrom my_package import module_afrom my_package import module_a
运行相对导入的文件 1 2 3 4 5 6 7 8 9 10 11 import sysfrom pathlib import Pathsys.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 from .module_a import func_a, ClassAfrom .module_b import func_b__all__ = ['func_a' , 'ClassA' , 'func_b' ] print ("my_package 已加载" )VERSION = "1.0.0"
实战案例 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 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__ = "程序员晚枫" __all__ = ['main_function' , '__version__' ]
包级别的配置 1 2 3 4 5 6 7 8 9 10 11 12 13 14 import logginglogger = 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:
1 2 3 4 5 6 7 8 9 10 11 12 13 pip install pip-tools requests pandas numpy 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 from setuptools import setup, find_packagessetup( 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 [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 python -m venv venv venv\Scripts\activate source venv/bin/activatedeactivate pip install virtualenv virtualenv venv 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 - 一个实用的工具包""" __version__ = "0.1.0" __author__ = "程序员晚枫" from .core import main_function__all__ = ['main_function' , '__version__' ]
1 2 3 4 5 6 7 8 9 10 """核心功能""" 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 """命令行接口""" import argparsefrom .core import main_functiondef 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 """工具函数""" 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 [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 twine upload --repository testpypi dist/* twine upload dist/* pip install my_tool
高级技巧 动态导入 1 2 3 4 5 6 7 8 9 10 11 12 13 14 import importlibmodule = 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 import importlibfrom pathlib import Pathdef 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 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 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 def my_function (): pass import json json.dumps({})
解决 :避免使用标准库名作为模块名
坑3:相对导入在脚本中失败 坑4:忘记init .py 推荐:AI Python零基础实战营 想系统学习Python工程化开发?
课程内容:
✅ Python基础语法 ✅ 模块与包管理 ✅ 项目结构设计 ✅ 实战项目练习 🎁 限时福利 :送《Python编程从入门到实践》实体书
👉 点击了解详情
相关阅读 PS:好的代码组织能让项目更易维护。记住:高内聚、低耦合、职责单一、避免循环导入。
📚 推荐教材 主教材 :《Python 编程从入门到实践(第 3 版)》
📚 推荐:Python 零基础实战营 系统学习Python,推荐这个免费入门课程 👇
特点 说明 🎯 专为0基础设计 门槛低,上手快 📹 配套视频讲解 配合文章学习效果更好 💬 专属答疑群 遇到问题有人带 🎁 实体书赠送 优秀学员送《Python编程从入门到实践》
👉 点击免费领取 Python 零基础实战营
💬 联系我 主营业务 :AI 编程培训、企业内训、技术咨询
🎓 AI 编程实战课程 想系统学习 AI 编程?程序员晚枫的 AI 编程实战课 帮你从零上手!