
一个空文件夹摆在面前就像一张白纸摆在作家面前。你盯着它心里涌起无数种可能也涌起无数种恐惧。第一行代码该写什么包名怎么起是搞个src目录还是直接平铺这种窒息感不是新手专属老手只不过把恐惧藏在了熟练的指尖下。我见过太多项目最初的激动被三周后的混乱吞噬最后连作者本人都懒得打开。结构不是装饰品它是在为未来的每一个凌晨三点负责任的。项目结构从来不是先想清楚的而是长出来的但需要一个不会扼杀生命的土壤。这句话我反复咀嚼了五年直到自己踩过所有坑才敢说出口。很多人一上来就翻Cookiecutter模板搬来一套“最佳实践”结果代码没写几行先被庞大的目录树吓住了。还有更多人什么都不管直接在根目录堆了十几个.py文件等到要打包发布时欲哭无泪。这两种极端之间藏着真正的手艺。先写代码还是先搭骨架我年轻时喜欢先搭骨架目录建得层层叠叠像是城市规划师画了张未来二十年的大饼。问题是你还没生活过怎么知道哪里该是卧室哪里该是厨房没有代码的架构就是空中楼阁迟早要推倒重来。反过来完全不考虑结构代码写到一周就会陷入“到处找import”的泥潭。我的解决之道是“四步启动法”。第一步只建一个主包目录和一个测试目录主包下放一个空的__init__.py测试目录放一个什么都测的test_smoke.py。第二步把第一个功能模块直接写进主包不要分太多层。第三步当代码超过200行或当你发现某个函数开始在多个地方被引用时自然分裂出第一个子模块。第四步在每次分裂的瞬间顺手把测试文件也裂开。架构是重构出来的不是规划出来的这句话在项目前两周尤其成立。那种一上来就建十几个目录的做法本质上是一种防御性编程但防御的敌人不是未来需求而是自己的焦虑。空目录不会给你任何安全感它只会让git status看起来像一片无意义的森林。真正让结构立起来的是包之间清晰的依赖关系而依赖关系只能从真实运行的代码中涌现。模块划分的边界感模块划分算得上搭结构里最玄学的部分。有人按技术分层controllers、services、models、utils层级分明但业务逻辑被切得稀碎有人按业务划分order、user、payment内聚高但容易产生跨越各层的公共基座。没有唯一正确答案只有当前阶段最少的摩擦系数。我现在的默认偏好是按业务域划分每个业务域是一个包包内再按需要分层。比如一个电商项目根目录下就是order/、user/、inventory/。每个包里放自己的models.py、service.py、api.py。这样做的最大好处是当你修改订单逻辑时大部分改动都发生在order/目录内不会波及到user/。边界清晰的意义不在于“好看”而在于让每次git diff都像是精确的手术刀。但这里有一个陷阱就是业务域之间的共享代码。两个包都用到了同一个装饰器或者同一个计算税金的函数该放哪有人会立刻抽出一个common/或utils/目录然后塞满各种互不相干的小工具。我见过最可怕的utils.py两千多行里面既有字符串处理又有数据库连接甚至还有一个发邮件的函数。垃圾回收站式的utils是项目腐化的起点。正确的做法是把共享逻辑下沉到更基础的层或创建一个有明确语义的包比如pricing.py、notifications.py。如果找不到一个词来精准命名那个共享代码那说明它根本不应该被共享。配置文件不是随便写写很多人把配置文件当成烫手山芋放根目录的config.ini里或者直接用环境变量又或者在代码里写一个CONFIG {...}。这些做法在早期项目里都没问题但结构会因此悄悄变形。配置是关于“环境差异”的声明它应该是唯一跟环境对话的入口。我倾向于用一个独立的settings.py或config/包来统一管理。这里说的管理不是让你手写一大堆变量赋值而是制定规则哪些值必须由环境变量覆盖哪些值有默认值哪些值在测试环境下要强制关闭。比如数据库URL默认是本地SQLite但如果设置了DATABASE_URL环境变量就替换它。这一层逻辑集中起来其他模块永远只做from settings import DATABASE_URL。当配置项超过二十个时再拆成一个config/包拆成base.py、development.py、production.py每个环境继承base的默认值覆盖掉特定字段。最忌讳的是把配置项散落在各个模块内部某个API的密钥藏在service/wechat.py的常量里另一个认证超时时间写在utils/auth.py里。等到换一个环境部署你要翻遍整个项目找那三五个神秘变量。项目结构的一大职责就是让你能在一分钟内定位“环境相关的东西在哪”。做不到这一点结构就只是摆设。测试代码的位置就是项目的肝脏肝脏是解毒器官测试代码则是项目的排毒系统。但测试目录怎么放直接影响你写测试的意愿。我见过把tests放在src内部的也见过跟业务包完全平铺的还有用tests/unit/、tests/integration/分开的。测试目录的层级应该跟主包结构保持镜像而不是另起炉灶。假设主包叫myapp测试就放在tests/test_myapp/下tests/test_myapp/test_order.py对应myapp/order.py。这样当你改完order.py你会立刻知道该去哪个文件补测试。不写测试的理由千千万万找不到测试文件是最蠢的那个。镜像结构让你在这个动作上零思考而零思考是养成习惯的关键。另外测试里还需要一个固定的conftest.py来放fixtures。早期项目我喜欢直接在每个测试文件里新建测试数据结果大量重复改一个字段要改几十处。后来把公共fixture收敛到tests目录的conftest.py里每个测试文件只留自己特有的fixture。测试代码也是代码它需要跟生产代码一样被对待而不是杂乱的脚本堆。但别误解我的意思我不主张测试代码过度设计。测试应该直接、简单、一目了然任何复杂的封装都会让测试失去意义。虚拟环境与依赖锁定虚拟环境是项目结构的地基但它太容易被忽视。很多教程告诉你创建完文件夹就python -m venv venv然后pip install。可一旦进入真实项目你马上会碰到依赖地狱。就算全世界都在用Poetry或PDM你也不得不承认pip加requirements.txt仍然是最通用的最小共同点。我的做法分两层。第一层是requirements.in里面写直接依赖的名称和版本范围比如flask3.0,4这是给人类看的标明这个项目意图用什么库。第二层是requirements.txt通过pip freeze或pip-compile生成把所有传递依赖的完整版本钉死这是给机器和部署环境用的。这个分离带来的好处是升级依赖时只需改.in文件然后重新编译不会把乱七八糟的依赖顺便升级。虚拟环境本身应该被.gitignore忽略掉但需要一个environment.yml或pyproject.toml来声明Python版本。如果你的项目依赖某些系统库还要写清楚它们。别指望半年后的同事甚至三个月后的你还记得当初需要libpq-dev。把环境搭建步骤写进README比任何架构图都更实在。命名是结构的灵魂结构有形但命名赋予它神。包名、模块名、类名、函数名这些名字组合在一起构成了开发者脑海中关于项目的隐喻地图。一个命名混乱的项目即使目录层级再合理也会让人迷路。我坚持三条命名规则。第一包名用小写不加下划线尽量一两个词如auth、billing。第二模块名用短下划线分词但要克制user_profile.py可以user_profile_data_processor.py就过分了。第三类名用大驼峰但类的职责必须跟包名呼应。比如auth包下的OAuthLoginService比auth包下的Handler或Manager要明确得多。最糟糕的命名是用技术术语掩盖业务含义。你看到BaseController不知道它管什么看到DataTransformer不知道自己该不该用。好的名字是自解释的它应该让新手不看文档也能猜出七八分。当一个名字需要你加一大段注释来解释时说明这个名字是错的换个词或者重新审视模块边界。命名也需要迭代。不要在项目开始时就强迫自己给每个类想一个完美名字那是浪费时间。写代码的时候先用一个临时名比如Thing或Processor等代码稳定下来再改。但一旦项目发布修改公共API的名字代价就大了所以核心模块命名要特别谨慎宁可在早期多花十分钟讨论也不要在后期痛苦迁徙。从第一天就引入入口和入口函数几乎每个Python项目都会面临一个问题代码该怎么启动是python main.py还是python -m myapp还是flask run项目结构必须明确回答“怎么运行”这个答案要被写死在README的最顶部。我习惯于在项目根目录放一个极薄的main.py里面只有一句话from myapp.cli import main; main()。真正的工作在myapp/cli.py里。这个做法看起来多此一举但它有巨大优势任何人在项目根目录敲python main.py就能跑起来不依赖环境变量传递模块路径也不用理解包内部的入口点在哪。入口文件越薄项目结构就越耐人寻味。同样重要的还有包内的__init__.py。很多新手把__init__.py当摆设或者在里面写一堆导入。不__init__.py是一个包的门面它应该暴露包的公共API而不是暴露内部实现细节。比如myapp/__init__.py里写from .version import __version__就够了不要把所有模块都导入进来否则会导入一堆不需要的依赖拖慢启动速度还会造成循环导入。重构不是大型灾难片项目结构不是一次定稿的它需要持续调整但很多人对调整怀有恐惧仿佛把文件挪个位置就会引发雪崩。实际上Python的动态特性给了你足够的活动空间只要你保持测试覆盖重命名模块是件很安全的事。我的经验是每次提交代码时顺手问三个问题这个文件是否还在做它名字说的事这个包是否还有外部依赖有没有重复的模块被放到一起了如果答案暧昧就趁早动手。别攒着“等下一阶段统一处理”技术债的利息在结构层面是复利增长越拖越贵。不过重构也要讲究节奏。千万不要在项目上线前夜大兴土木也不要在周五下午开始动核心包的边界。把重构当成一种持续的小动作而不是一次惊天动地的工程。小步重构、频繁提交、保证测试绿灯这样项目结构才能像活物一样呼吸生长。结构终究是团队契约就算你单人开发几个月后你也会成为自己的陌生人。项目结构不是写给你一个人的它是写给未来的每个读者。当团队协作时结构更是无言的契约。结构决定了你的同事在哪儿找东西也决定了他们不敢把东西放哪儿。没有清晰结构每个人随心所欲地创建目录和文件两三个月后项目就变成垃圾场。你需要一份极简的CONTRIBUTING.md告诉新人新代码放哪个包测试怎么跑配置怎么改。但更重要的是你要营造一种文化——改动结构要像改动公共API一样谨慎因为结构本身就是一个公共API。当有人提议“我建在一个新目录下比较方便”时你要警惕那可能意味着现有结构无法容纳他想做的事你需要理解他的意图而不是轻易说“可以啊”。回望这些年踩过的坑我最深的感触是项目结构不是束之高阁的图纸而是你和未来自己的一次深度对话。从零开始搭建别怕走弯路别怕改结构。只要保持代码与结构的同频保持命名与语义的一致保持测试与模块的镜像你的项目就会像一棵树一样在必要的修剪中越长越结实。那种看着杂乱目录逐渐变得井然有序的快感抵得过一百次深夜修bug的煎熬。你搭建的不是目录和文件而是思考和协作的容器。容器对了里面的酒才会越酿越醇。