news 2026/6/26 13:22:01

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
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__"只是给了你一个选择的机会,但选择权在你手里。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/6/26 13:21:40

Zotero GPT插件架构解析:AI驱动的文献管理性能优化指南

Zotero GPT插件架构解析:AI驱动的文献管理性能优化指南 【免费下载链接】zotero-gpt GPT Meet Zotero. 项目地址: https://gitcode.com/gh_mirrors/zo/zotero-gpt 在当今数字化科研环境中,文献管理工具正经历着从传统存储到智能分析的范式转变。Z…

作者头像 李华
网站建设 2026/6/26 13:20:44

嵌入式GUI开发利器:emWin仿真API详解与实战集成指南

1. 项目概述:为什么嵌入式GUI开发离不开仿真?在嵌入式系统开发,尤其是带图形用户界面的项目中,硬件依赖一直是个头疼的问题。想象一下,你正在为一个智能家电或者工业HMI设计界面,每次修改一个按钮的颜色、调…

作者头像 李华
网站建设 2026/6/26 13:17:16

CVE-2025-54068 — Laravel Livewire v3 远程代码执行漏洞 完整分析

CVE-2025-54068 — Laravel Livewire v3 远程代码执行漏洞 完整分析 一、基本信息 项目内容CVE IDCVE-2025-54068QVD IDQVD-2025-28233CNNVD IDCNNVD-202507-2331漏洞类型反序列化 → 远程代码执行 (RCE)CWECWE-502(不可信数据反序列化)影响产品Larave…

作者头像 李华
网站建设 2026/6/26 13:02:27

Linux服务器开源杀毒软件实战:ClamAV配置与性能调优指南

1. 项目概述:为什么Linux服务器也需要杀毒软件? 很多刚接触Linux运维的朋友,包括我自己在早期,都有一个根深蒂固的观念:Linux系统是“百毒不侵”的。毕竟,我们很少听说哪个Linux桌面用户中病毒,…

作者头像 李华
网站建设 2026/6/26 13:01:08

鸣潮游戏AI自动化助手:ok-ww开源项目深度解析与实战指南

鸣潮游戏AI自动化助手:ok-ww开源项目深度解析与实战指南 【免费下载链接】ok-wuthering-waves 鸣潮 后台自动战斗 自动刷声骸 一键日常 Automation for Wuthering Waves 项目地址: https://gitcode.com/GitHub_Trending/ok/ok-wuthering-waves ok-ww是一个基…

作者头像 李华