Python 项目布局实战:从单文件脚本到 Web 应用怎么摆

2026-05-05 26 预计阅读时间:1 分钟
来源:realpython.com AI 摘要 原文链接

免责声明:本文为 AI 摘要整理,建议结合原文阅读。摘要可能省略上下文、版本差异或边界条件,不作为官方说明。

预计阅读时间:9 分钟

一个 Python 项目文件放哪、目录怎么组织,看似小事,却直接影响包能不能装、测试能不能跑、新人能不能快速上手。不同类型的项目——一次性脚本、可安装包、内部库、Django/Flask Web 应用——各有成熟的布局套路。下面逐一拆解,并给出可直接套用的目录模板和配置示例。

一次性脚本:别小看单文件

数据分析、运维小工具、一次性迁移脚本,往往就一个 .py 文件。但"一个文件"不代表可以乱堆。

推荐布局:

my_script/
├── my_script.py
├── requirements.txt
├── README.md
└── .gitignore

关键点:

  • requirements.txt 锁依赖,哪怕只有两行也别省。半年后重跑时你会感谢自己。
  • README.md 写清楚运行方式:python my_script.py --input data.csv
  • 脚本内部用 argparse 而不是硬编码路径,方便复用。

一个可复用的脚本骨架:

#!/usr/bin/env python3
"""一次性数据清洗脚本 —— 把原始 CSV 中的空行删掉并输出清洗后文件."""

import argparse
import csv
import sys


def clean_csv(input_path: str, output_path: str) -> None:
    with open(input_path, newline="", encoding="utf-8") as fin:
        reader = csv.reader(fin)
        rows = [row for row in reader if any(cell.strip() for cell in row)]

    with open(output_path, "w", newline="", encoding="utf-8") as fout:
        writer = csv.writer(fout)
        writer.writerows(rows)

    print(f"清洗完成:{len(rows)} 行写入 {output_path}")


def main() -> None:
    parser = argparse.ArgumentParser(description="清洗 CSV 空行")
    parser.add_argument("--input", required=True, help="原始 CSV 路径")
    parser.add_argument("--output", required=True, help="输出 CSV 路径")
    args = parser.parse_args()
    clean_csv(args.input, args.output)


if __name__ == "__main__":
    main()

把业务逻辑放在函数里、入口放在 main(),以后想改成可安装包时只需加一个 setup.py,代码本身不用动。

可安装包:标准 src 布局

当你需要 pip install 自己的包、发布到 PyPI 或内部 artifact 仓库时,布局必须满足打包工具的约定。目前社区主流是 src layout——源码放在 src/ 子目录下,而不是项目根目录。

my_package/
├── src/
   └── my_package/
       ├── __init__.py
       ├── core.py
       └── utils.py
├── tests/
   ├── test_core.py
   ├── test_utils.py
├── pyproject.toml
├── README.md
├── LICENSE
└── .gitignore

为什么用 src layout?根目录下直接放包目录(flat layout)容易导致一个致命问题:本地开发时 import my_package 导入的是源码目录而非已安装的包,测试可能跑过了但安装后行为不同。src/ 强制你先安装再测试,杜绝这类隐患。

pyproject.toml 最小可运行示例:

[build-system]
requires = ["setuptools>=68.0", "wheel"]
build-backend = "setuptools.build_meta"

[project]
name = "my_package"
version = "0.1.0"
description = "一个演示标准布局的示例包"
requires-python = ">=3.9"
dependencies = []

[project.optional-dependencies]
dev = ["pytest>=7.0", "ruff"]

[tool.setuptools.packages.find]
where = ["src"]

安装并测试的完整流程:

# 创建虚拟环境并安装包(可编辑模式)
python -m venv .venv
source .venv/bin/activate
pip install -e ".[dev]"

# 运行测试 —— 此时 import 的是已安装的包
pytest tests/

内部库:公司私有包的布局

内部库不发布到 PyPI,只给公司其他项目引用。布局和可安装包基本一致,区别在于分发方式:通常用 Git 仓库直接引用,或推到私有 PyPI 服务器。

pyproject.toml 里加一行标记私有:

[project]
name = "company_auth_lib"
version = "0.3.2"
# 私有包不设 classifiers 里的 PyPI 发布标记
classifiers = ["Private :: Do Not Upload"]

其他项目引用时,requirements.txtpyproject.toml 直接写 Git 地址:

[project]
dependencies = [
    "company_auth_lib @ git+ssh://git@gitlab.internal.dev/company-auth-lib.git@v0.3.2",
]

目录结构和可安装包一致,只是不需要 LICENSE(公司内部协议替代)和 PyPI 发布配置。

Flask 应用:两层结构起步

Flask 应用从小项目到大项目跨度很大。起步时一个 app.py 就能跑,但到需要蓝图、模板、静态文件时,必须升级布局。

推荐的中等规模布局:

flask_app/
├── app/
   ├── __init__.py      # create_app() 工厂函数
   ├── models.py
   ├── auth/
      ├── __init__.py
      ├── routes.py
      ├── forms.py
   ├── blog/
      ├── __init__.py
      ├── routes.py
      ├── templates/
         └── blog/
             ├── list.html
             ├── detail.html
   ├── templates/       # 全局模板
      └── base.html
   ├── static/
      ├── css/
      ├── js/
├── tests/
   ├── conftest.py
   ├── test_auth.py
   ├── test_blog.py
├── run.py               # 入口:from app import create_app; app = create_app(); app.run()
├── pyproject.toml
├── requirements.txt     # 生产依赖锁定
└── .flaskenv            # FLASK_APP=run.py 等环境变量

工厂函数示例——app/__init__.py

from flask import Flask
from flask_sqlalchemy import SQLAlchemy

db = SQLAlchemy()


def create_app(config_name: str = "default") -> Flask:
    app = Flask(__name__)
    app.config.from_object(f"app.config.{config_name}")

    db.init_app(app)

    from app.auth.routes import auth_bp
    from app.blog.routes import blog_bp

    app.register_blueprint(auth_bp, url_prefix="/auth")
    app.register_blueprint(blog_bp, url_prefix="/blog")

    return app

蓝图注册让每个功能模块自包含:自己的路由、模板、表单。新增模块只需加一个蓝图目录和一行 register_blueprint,不用改其他代码。

Django 应用:项目与应用的分层

Django 的约定更明确:project 是整体配置容器,app 是功能模块。两者目录层级不同。

django_project/
├── manage.py
├── mysite/                  # project 包
│   ├── __init__.py
│   ├── settings/
│   │   ├── base.py
│   │   ├── dev.py
│   │   ├── prod.py
│   ├── urls.py
│   ├── wsgi.py
│   ├── asgi.py
├── blog/                    # app
│   ├── __init__.py
│   ├── models.py
│   ├── views.py
│   ├── urls.py
│   ├── admin.py
│   ├── migrations/
│   ├── templates/blog/
│   │   ├── post_list.html
│   │   ├── post_detail.html
│   ├── tests/
│   │   ├── test_models.py
│   │   ├── test_views.py
├── requirements/
│   ├── base.txt
│   ├── dev.txt
│   ├── prod.txt
└── pyproject.toml

几个容易踩的坑:

  • 不要把所有 models 堆在 project 目录下。每个 app 应有自己的 models.py,Django 的迁移系统按 app 管理迁移文件。
  • settings 拆分比单文件 settings.py 更实用。base.py 放公共配置,dev.pyprod.py 分别覆盖数据库、密钥等。启动时用 DJANGO_SETTINGS_MODULE=mysite.settings.prod 切换。
  • app 的 templates 放在 app 目录内blog/templates/blog/),而不是全局 templates/。这样模板命名空间天然隔离,避免 list.html 冲突。

创建新 app 并注册的命令:

# 在项目根目录执行
python manage.py startapp comments
# 然后在 settings/base.py 的 INSTALLED_APPS 里加上 "comments"

选布局的决策清单

拿到一个新项目时,按这个顺序判断:

项目类型 布局选择 核心标志
一次性脚本,不复用 单文件 + requirements.txt if __name__ == "__main__"
会迭代的数据工具 脚本骨架 + argparse,考虑升级为包 函数拆分、有 CLI 入口
给他人 pip install 的库 src layout + pyproject.toml src/ 子目录、pytest 跑已安装包
公司内部共享库 同 src layout,Git/私有 PyPI 分发 Private :: Do Not Upload
Flask 中小应用 工厂函数 + 蓝图目录 create_app()register_blueprint()
Django 应用 project/app 分层 + settings 拆分 manage.py、多 app 目录

最后一条通用原则:不管哪种布局,测试目录永远独立于源码目录tests/ 放在项目根目录,和 src/app/ 平级。这样测试可以独立运行,也不污染包的安装内容。

布局不是审美问题,是工程约束。选对了,打包、测试、部署各环节才能顺畅衔接;选错了,每个环节都会冒出意想不到的路径问题。按项目类型套用对应模板,再根据团队规模微调,比从零摸索省掉大量排错时间。


相关推荐