当前位置: 首页 > news >正文

050、模块与包组织结构:单文件到大型项目的目录演进与 main

050、模块与包组织结构:单文件到大型项目的目录演进与main

上周帮一个朋友调试他的爬虫项目,代码全堆在一个spider.py里,三千多行。他跑起来没问题,但想加个定时任务就炸了——if __name__ == "__main__"那段逻辑被重复执行,日志刷屏,数据库连接池直接爆掉。我扫了一眼,他压根没理解模块导入时 Python 到底在干什么。这其实是个很典型的坑,从单文件写到多文件,再到包结构,很多人卡在“为什么我的代码跑了两遍”这个问题上。

单文件时代的“舒适区”

刚学 Python 时,我们都在一个.py文件里写所有东西。函数、类、全局变量、执行逻辑,全挤在一起。这没什么不对,对于几十行的小脚本,单文件反而是最清晰的。

# 一个典型的单文件脚本importrequestsdeffetch_data(url):returnrequests.get(url).json()defprocess_data(data):return[item['name']foritemindata]# 这里踩过坑:直接写执行代码url="https://api.example.com/data"raw=fetch_data(url)result=process_data(raw)print(result)

问题在于,当你把这个文件作为模块导入到另一个文件时,最后那三行代码会立刻执行。这就是if __name__ == "__main__"存在的意义——它像一道门,只有当你直接运行这个文件时,门才打开;作为模块导入时,门是关着的。

别这样写:把执行逻辑裸写在文件底部,没有任何保护。你永远不知道哪天别人会from your_script import fetch_data,然后你的脚本就自顾自跑起来了。

从单文件到多文件:模块的诞生

当代码超过两百行,你就该考虑拆分了。拆分的依据不是“按文件大小”,而是“按职责”。比如爬虫项目,可以拆成fetcher.pyparser.pystorage.py

# fetcher.pyimportrequestsdeffetch(url):print(f"正在请求:{url}")# 调试用,别删returnrequests.get(url,timeout=10).text
# parser.pyfrombs4importBeautifulSoupdefparse_title(html):soup=BeautifulSoup(html,'html.parser')returnsoup.title.stringifsoup.titleelse"无标题"
# main.pyfromfetcherimportfetchfromparserimportparse_titleif__name__=="__main__":html=fetch("https://example.com")title=parse_title(html)print(title)

这里有个隐藏细节:from fetcher import fetch这行代码执行时,Python 会从头到尾执行fetcher.py。如果fetcher.py底部有if __name__ == "__main__"保护的代码,那不会执行;但如果没有保护,就会执行。这就是为什么我强调“执行逻辑必须放在__main__块里”。

包:目录即模块

当文件多起来,平铺在同一个目录下会变得混乱。这时候需要包——本质上就是一个包含__init__.py的目录。

my_project/ ├── __init__.py ├── fetcher/ │ ├── __init__.py │ ├── http.py │ └── selenium.py ├── parser/ │ ├── __init__.py │ ├── html_parser.py │ └── json_parser.py └── main.py

__init__.py可以是空文件,但别真的留空。我习惯在里面写包的文档字符串和__all__列表,这样from package import *时不会把内部函数全暴露出去。

# fetcher/__init__.py""" 网络请求模块 提供 HTTP 和 Selenium 两种抓取方式 """__all__=['fetch_http','fetch_selenium']from.httpimportfetch_httpfrom.seleniumimportfetch_selenium

注意这里的.http是相对导入。相对导入只能在包内部使用,不能在main.py里用from .fetcher import ...,因为main.py不是包的一部分。这个限制让很多人困惑——简单记:包内部的模块之间用相对导入,包外部的入口文件用绝对导入。

__main__的两种形态

if __name__ == "__main__"这个写法大家都会,但它的行为在不同场景下有微妙差异。

场景一:直接运行文件

python main.py

此时__name__等于"__main__",块内代码执行。

场景二:作为模块运行

python-mmy_project.main

此时__name__等于"my_project.main",块内代码不执行。但注意,-m方式会把当前目录加入sys.path,所以包内的相对导入能正常工作。

场景三:被其他模块导入

frommy_project.mainimportsome_function

此时__name__"my_project.main",块内代码不执行。

我见过最离谱的 bug 是有人把测试代码写在if __name__ == "__main__"外面,然后 CI 跑测试时,测试框架导入模块,那些测试代码就自动执行了,导致测试结果全是假的。

大型项目的目录演进

当项目超过十个模块,目录结构需要更精细的设计。我常用的模式是这样的:

project/ ├── src/ │ ├── __init__.py │ ├── core/ # 核心业务逻辑 │ ├── utils/ # 工具函数 │ ├── models/ # 数据模型 │ └── services/ # 服务层 ├── tests/ │ ├── __init__.py │ ├── test_core/ │ └── test_utils/ ├── scripts/ # 运维脚本 │ ├── deploy.py │ └── migrate.py ├── config/ │ ├── __init__.py │ ├── dev.py │ └── prod.py ├── setup.py └── requirements.txt

关键点:src目录下的代码是“可导入的”,scripts目录下的代码是“可执行的”。scripts里的脚本通常直接写执行逻辑,不需要__main__保护,因为它们就是被直接运行的。

别这样写:把scripts里的脚本也加上if __name__ == "__main__",然后期望别人python -m scripts.deploy来运行。这反而增加了心智负担。脚本就是脚本,直接python scripts/deploy.py就好。

一个真实的调试案例

回到开头那个朋友的爬虫项目。他的目录结构是这样的:

spider/ ├── __init__.py ├── spider.py # 三千行,包含所有逻辑 ├── config.py └── run.py # 只有一行:from spider import run; run()

问题出在spider.py里:

# spider.py 底部if__name__=="__main__":run()# 启动爬虫

run.pyfrom spider import run这行,会执行spider.py的顶层代码。如果spider.py里有全局变量初始化、数据库连接等操作,这些都会在导入时执行。更糟的是,他用了multiprocessing,子进程又会重新导入spider.py,导致__main__块里的代码在子进程里也执行了。

解决方案很简单:把spider.py拆成多个模块,run.py只负责导入和调用,所有执行逻辑都放在if __name__ == "__main__"里。但更根本的问题是,他一开始就没想清楚“哪些代码是定义,哪些代码是执行”。

个人经验性建议

  1. 每个.py文件都应该能被安全导入。意思是,即使这个文件底部有if __name__ == "__main__"块,导入它时也不应该产生副作用。全局变量初始化、日志配置、数据库连接这些,要么放在__main__块里,要么用懒加载。

  2. __init__.py不是摆设。我见过太多人留空文件。至少写上__all__和包级别的导入,这样from package import *时不会把内部模块全暴露出来。更讲究一点,可以在__init__.py里做版本检查或环境初始化。

  3. 相对导入只用在包内部from . import sibling这种写法,只能在包内的模块里用。入口文件(比如main.pyrun.py)永远用绝对导入。这个规则能避免 90% 的导入错误。

  4. 别在__main__块里写太多逻辑if __name__ == "__main__"里应该只调用一个main()函数,或者解析命令行参数后调用main()。把具体逻辑写在函数里,方便测试也方便复用。

  5. 测试代码不要写在__main__块里。写测试就用pytestunittest,别图省事把测试代码写在if __name__ == "__main__"里。你永远不知道什么时候测试代码会被意外执行。

最后,记住一个原则:模块是定义,脚本是执行。一个.py文件要么是模块(被导入),要么是脚本(被运行),不要试图同时做好两件事。if __name__ == "__main__"只是给了你一个选择的机会,但选择权在你手里。

http://www.jsqmd.com/news/1081671/

相关文章:

  • 热门AI论文工具势力榜(2026 真实数据)
  • SSD时钟源选型与宽温振荡器工程实践
  • 周纪四(第2部分,共2部分)
  • 芯片烧录:校验与验证如何确保零错误?
  • 如何彻底解决Reloaded-II模组依赖循环问题:3步终极指南
  • Web安全实战:从SQL注入到应急响应,构建知攻善防能力
  • P89LPC91x单片机I2C接口开发实战:从寄存器配置到状态机实现
  • SPRING优化算法中动量参数μ的稳定性分析与PRIME-SR自适应控制方法
  • 嵌入式GUI开发利器:emWin仿真API详解与实战集成指南
  • 终极中文汉化指南:让Royal TSX远程管理工具告别英文界面困扰
  • 嵌入式GUI开发:位图与字体资源优化转换实战指南
  • 嵌入式GUI输入驱动开发:从emWin PID API到触摸屏、键盘实战
  • 3分钟配置完成的终极中国象棋AI辅助系统:告别手动输入,拥抱智能对弈
  • 全国大棚类型分布图:北方为啥都建日光温室,南方为啥全是冷棚?
  • Java程序员拿失业金空窗近 3 个月没躺平!一边接外包练手,一边自研 AI Agent 面试训练系统,聊聊数据资产才是 Agent 的核心命脉
  • 不当获利金额红线解析:从民事到刑事的法律边界与风险自检
  • VMware替代方案决策树(2024修订版):按虚拟机规模/合规要求/现有技能栈自动匹配最优解
  • 手机端系统镜像提取技术突破:Payload-Dumper-Android实现零依赖OTA解析
  • [实战指南] 2026年制造业FAI流程中CAD图纸气泡图的自动识别与检验计划规范
  • AI 领域「落盘」完整解释
  • 3种简单方法免费激活Beyond Compare 5:开源密钥生成工具完全指南
  • DockDoor完全指南:如何通过macOS窗口预览功能提升工作效率
  • Windows 11硬件限制终极绕过指南:一键升级老旧电脑的完整方案
  • 免费文档下载终极指南:一键获取30+文库平台资源
  • 碧蓝航线Live2D提取终极指南:从游戏资源到可编辑模型的完整教程
  • 从零构建解释器:深入理解编程语言运行机制与实现原理
  • 5个关键优势:DiskInfo现代硬盘监测工具全面解析与使用指南
  • 树莓派计算模块外设连接与设备树配置实战指南
  • LPC213x I2C总线异常状态解析与鲁棒性驱动开发实战
  • 粘性耗散和黏性耗散哪个更准确——在力学的规范术语体系中,描述流体这种物理性质的标准用字为“黏性”,对应英文viscosity,“黏性耗散”是权威教材、专业文献中统一采用的表述:流体流动时,黏性应力做功