Python 学习笔记:具体项目实践
[!NOTE] 笔记说明
在阅读完了《[[基础语法学习|Python 学习笔记:基础语法学习]]》之后,相信读者已经初步体验到了 Python 社区所推崇的“优雅、明确、简单”的编码风格。在接下来的这篇笔记中,我们将会基于这些原则,逐步从脚本式代码过渡到结构清晰、依赖可控、可分发的 Python 项目。本篇将聚焦于:
- 介绍如何遵循 PEP 规范(例如 PEP 8)来构建 Python 项目;
- 介绍如何基于虚拟环境来安装并管理项目依赖(基于 uv 和 pip);
- 介绍将项目打包为可执行文件或可分发的扩展包(基于 uv);
PEP 规范与项目结构设计
在我开始学习 Python 的那个年代,这门编程语言在大多数使用场景下仍然被当作一种用于编写单一脚本文件的工具,而非用于构建完整项目的开发语言。换言之,那时候所谓的 Python 程序往往都只是若干独立脚本的集合,并不构成严格意义上的“项目”,自然也谈不上系统化的项目结构设计。这并非偶然。Guido van Rossum 在最初设计 Python 时,其核心目标之一,正是用一种语法简洁、可读性良好的高级语言,来替代 Unix Shell 以完成常见的系统管理与自动化任务。在这样的设计初衷下,Python 更强调“快速完成一件事”,而非长期维护的大规模工程组织。
然而,随着 Python 在近十余年间在科学计算、Web 开发、数据分析以及工程自动化等应用领域的蓬勃发展,人们对它的使用方式发生了根本性的变化。越来越多的开发者开始使用 Python 来构建功能复杂、生命周期较长的系统,这也使得代码的组织方式、模块边界以及项目结构设计逐渐成为一个无法回避的问题。因此,在讨论具体项目的实践之前,我们需要先解决一个经常被初学者忽视、但在真实工程中极其关键的问题:代码应该如何被组织,以及应当遵循怎样的规范来书写。
在如今的 Python 社区中,人们针对工程化的项目结构设计已经形成了一整套的可遵循的规范文档,这些文档被统称为 Python Enhancement Proposals,简称 PEP。当然,PEP 中的大部分内容是用于规范语言设计或解释器实现的,而我们在这里要讨论的是面向项目工程化实践的那一小部分,其目的是在项目结构设计方面建立起统一的约定,以便显著降低项目被理解的成本。这可以使开发者之间无需反复适应不同的代码组织习惯,从而把注意力集中在业务逻辑本身。下面,就让我们以当前主流的 PEP 8 为例,来具体介绍一下 Python 项目结构设计的基本原则。
从脚本到项目:结构上的转变
许多初学者在学习 Python 时,习惯于将所有代码写在单个 .py 文件中。这在学习阶段完全没有问题,但一旦进入真实而具体的项目,就会迅速暴露出可维护性上的瓶颈。而如果我们想要将自己的代码项目化,首先要做的就是安排好项目的目录结构。一个最基础、也是最常见的 Python 项目可以通过我们之前在《[[编程环境配置|Python 学习笔记:编程环境配置]]》一节中提到的 uv 项目管理工具来生成,其具体步骤如下:
-
在当前计算机的任意位置创建一个名为
python_demo的目录,并使用命令行终端程序打开该目录。 -
在命令行终端中执行
uv init命令,这样就得到了一个初步符合 PEP 8 规范的 Python 项目,其项目结构通常如下所示:python_demo ├── .python-version # 用于指定 Python 解释器版本号的文件 ├── pyproject.toml # 项目配置文件,用于填写一些项目元信息 ├── main.py # 项目入口代码,现在是一个 Hello, World! 程序 └── README.md # 项目说明文件,用于填写项目简介 -
虽然上述项目结构已经足以应对一些小型的开发需求了,但如果我们想让它更贴近当前社区普遍认可的 PEP 实践,还需要进一步调整。为此,我们需要继续在
python_demo目录下分别创建src和tests这两个子目录,并将之前位于项目根目录下的main.py文件移动到src目录中。调整后的项目结构如下所示:python_demo ├── .python-version # 用于指定 Python 解释器版本号的文件 ├── pyproject.toml # 项目配置文件,用于填写一些项目元信息 ├── README.md # 项目说明文件,用于填写项目简介 ├── src # 用于存放项目代码的目录 │ └── python_demo # 用于存放模块代码的子目录 │ ├── __init__.py # 用于声明当前子目录为模块的空文件 │ └── main.py # 模块入口代码,现在是一个 Hello, World! 程序 └── tests # 用于存放测试代码的目录上述项目结构体现了几个重要的工程原则:
- 将项目配置与其实现分离:项目的配置文件(如
pyproject.toml)应被放置在根目录下,而项目的实现源码则被集中放置在src目录中。 - 将项目的测试代码与其实现分离:测试代码位于独立的
tests目录中,避免与业务代码混杂。 - 项目结构的模块化:通过在特定的子目录中放置
__init__.py文件,将其声明为模块,从而使得 Python 解释器能够正确识别该目录。例如,如果我们在这里希望将src/python_demo子目录声明为一个模块,并将main.py文件中的hello函数暴露给外部,那么就需要在该__init__.py文件中添加以下代码:
from .main import hello - 将项目配置与其实现分离:项目的配置文件(如
当然,这里需要再次强调的是:PEP 8 中提出的这种项目结构设计规范不是强制标准,并不妨碍开发者们在组织项目方面的个性化发挥,但在生产环境中,它已经成为当前 Python 社区中被广泛认可的“最佳实践”之一。
为什么推荐使用 src 目录结构
读者可能会觉得有点疑惑,为什么我们需要将程序的实现代码放置一个独立 src 目录中?这种做法在项目结构设计上的现实意义是什么?简而言之,它的直接目的是建立一个独立的、结构化的代码组织机制,以便有效地避免以下问题:
- 意外导入当前目录下的同名模块;
- 在未正确安装项目时,代码“看似可运行但并不规范”;
- 部署或打包阶段出现路径相关的隐蔽错误。
换句话说就是,设置 src 目录可以迫使我们以“已安装包”的方式来使用自己的项目代码,这与真实的部署和分发场景高度一致。另外,在拆分模块时,我们也要注意不要一味地“越细越好”,应遵循以下几个简单但实用的原则:
- 每个模块只关注一类明确职责;
- 模块名应反映其用途,而不是实现细节;
- 避免在模块之间形成循环依赖;
- 对外暴露的接口应尽量稳定,内部实现可以自由调整。
总而言之,如果读者发现某个文件不断膨胀、同时承担多种职责时,往往就意味着它已经到了需要拆分的时刻。
在这一节中,我们并没有编写任何“功能性代码”,但所做的工作却直接决定了项目未来的可维护性上限。通过遵循 PEP 规范,并在一开始就建立清晰的项目结构,我们为后续的依赖管理、测试、打包与分发打下了坚实基础。在下一节中,我们将从工程实践的角度出发,介绍如何基于虚拟环境来安装并管理项目依赖,并说明为什么在真实项目中,依赖管理应当被视为项目设计的一部分,而不是事后补救的步骤。
项目依赖的安装与管理
在完成了上述项目结构设计的基本工作之后,我们接下来要解决的是另一个在具体项目实践中不可回避的问题:如何安装、隔离并稳定地管理项目依赖。对于 Python 项目而言,依赖管理不仅是要“能将项目所依赖的扩展包装上”,更重要的是要实现以下几个更本质的目标:
- 确保不同项目之间所依赖的扩展包互不干扰;
- 项目在不同机器、不同时间点上具有一致的运行环境;
- 项目所依赖的这些扩展包在版本上是可追溯、可复现的。
为了实现上述目标,Python 社区逐步形成了一套围绕虚拟环境(Virtual Environment)展开的通用实践。接下来,就让我们继续以 uv 这款项目管理工具为例,介绍现代 Python 项目中推荐的依赖管理方式。
使用 uv 来管理项目依赖
正如我之前在《[[编程环境配置|Python 学习笔记:编程环境配置]]》中所介绍的,在没有虚拟环境的情况下,Python 的第三方扩展通常会被安装到系统级解释器的 site-packages 目录中。这种做法在早期看似方便,但很快就会带来严重问题:
- 不同项目对同一依赖的版本要求可能冲突;
- 升级或卸载某个库可能意外破坏其他项目;
- 很难准确还原某个历史版本项目的运行环境。
虚拟环境的核心作用,就是为每个项目提供一个彼此独立的 Python 运行时环境,使解释器、第三方库以及相关工具只对当前项目可见。换言之,读者可以将虚拟环境理解为“项目的运行时沙箱”。但是,如果我们使用 Python 官方提供的解决方案,即先用venv 来创建虚拟环境,然后再用 pip 工具来安装并管理项目中的依赖,那么随着项目规模的扩大,我们很快就会遇到以下这些棘手问题:
- 项目所依赖的扩展包在版本上并未被明确锁定,这会导致项目在不同时空环境中的行为不一致;
- 不同机器上解析出的依赖树可能并不完全一致,这会导致项目在不同机器上的运行结果存在差异;
- 随着项目依赖的变化,虚拟环境中会出现越来越多的“孤儿依赖”,这会导致项目体积的日益臃肿。
为了更好地解决上述问题,在现代化的项目管理工具所提出的解决方案中,pip 工具通常不再被单独使用,而是用于充当“底层安装器”,配合 uv 这类更高层的依赖管理工具共同完成工作。在这里,我们可以将 uv 理解为一个集成了依赖解析、安装、锁文件生成等功能的工具链,它使得管理项目依赖的过程变得更加高效、稳定。除此之外,uv 在设计上也吸收了多种现有方案的优点,试着以更统一的方式解决依赖解析速度慢、环境复现不稳定、工具链分散、职责重叠等问题。
在 uv 的工作流中,项目依赖的管理逻辑通常是围绕 pyproject.toml 文件展开的。例如在我们之前创建的 python_demo 项目中,pyproject.toml 文件的初始内容如下所示:
[project] # 项目元信息配置声明
name = "python-demo" # 项目名称
version = "0.1.0" # 项目版本号
description = "Add your description here" # 项目简介
readme = "README.md" # 项目说明文件路径
requires.python = ">=3.13" # 项目最低支持的 Python 版本
dependencies = [] # 项目的直接依赖列表
接下来,如果我们像之前在《[[编程环境配置|Python 学习笔记:编程环境配置]]》一文中所演示的那样,执行了 uv add <扩展包名> 这一用于安装依赖的命令,那么 uv 就会自动修改 pyproject.toml 文件,将我们安装的扩展包及其当前的版本信息添加到 dependencies 列表中,并同步生成一个名为 uv.lock 文件,用于锁定项目依赖的版本,如图 1 所示:

图 1: 使用 uv 安装依赖
这时候,如果我们执行 uv tree 命令,就可以看到 uv 解析出的项目依赖树,如图 2 所示:

图 2: uv 解析出的项目依赖树
从图 2 中可以看到,当前项目所安装的直接依赖是 Flask,而 Flask 又依赖了 Werkzeug、Jinja2 和 itsdangerous 等若干个扩展包,其中的诸如 Werkzeug、Jinja2 扩展包又继续依赖了 MarkupSafe 这个扩展包。这些信息被 uv 以树状结构展示出来,使得读者可以直观地了解项目依赖的层次关系,这比传统做法中使用 pip list 命令所获得的扁平化依赖列表要直观得多。
更重要的是,当我们需要删除项目的某个直接依赖时,只需要执行 uv remove <扩展包名> 命令,uv 就会自动解析出该依赖所涉及的依赖树,并删除其中不再被其他依赖引用的间接依赖,这就解决了传统做法中使用 pip uninstall <扩展包名> 命令时,会产生的“孤儿依赖”问题,如图 3 所示:

图 3: 使用 uv 删除依赖
锁定依赖版本的重要性
当然了,在具体项目实践中,“能安装”并不等于“可复现”。为了确保项目在不同时间、不同环境中的行为一致,它所依赖的扩展包在版本上应当被明确锁定,而不是无限制地跟随最新版本更新。uv 在这方面通过创建 uv.lock 文件对版本锁定提供了原生支持,使依赖状态成为项目的一部分,而不是隐含在某台机器中。从工程化的角度来看,锁定依赖版本意味着项目的构建过程是可重复的,线上问题更容易回溯,协作成员的环境更加一致。这意味着,任何一个开发者只要完整地获得了一个项目的源码和配置文件,就可以通过执行 uv sync 这个命令,在本地完整地复现该项目的完整运行环境,如图 4 所示:

图 4: 使用 uv 复现项目运行环境
现在,让我们再来系统地为读者总结一下 Python 项目中依赖管理的基本思路:
- 虚拟环境是项目级隔离的基础设施;
- pip 负责执行安装,但并不擅长整体依赖管理;
- uv 提供了更现代、更稳定的依赖解析与管理能力;
- 依赖版本应当被视为项目设计的一部分,而非临时决策。
在下一节中,我们将进一步讨论:如何将一个已经结构清晰、依赖稳定的 Python 项目,打包为可执行文件或可分发的扩展包,并说明这些能力在具体项目实践中的意义。
项目的打包与分发
当一个 Python 项目完成了合理的目录结构设计,并且其依赖关系已经通过虚拟环境与锁文件得到了稳定的管理之后,我们接下来就该开始考虑项目的打包与分发问题了。请注意,“分发”这个词在工程化的语境中并不只是指把项目的源代码拷贝给别人,它通常还包含以下几种常见目标:
- 作为 Python 扩展包 分发,供其他项目通过依赖方式引入;
- 打包为 可执行命令行工具,供用户直接运行;
- 在某些场景下,进一步打包为 独立可执行文件,以减少运行环境依赖。
下面,我们将继续基于 uv 这款项目管理工具来为读者介绍当前 Python 社区所推荐的项目打包与分发方式。
使用 uv 来打包项目
在早期,人们将 Python 项目打包成第三方扩展的方式是高度分散的。有的项目依赖与 setup.py 文件,有的项目通过自定义脚本发布,有的项目甚至完全没有明确的打包入口。这些做法在项目规模较小时尚可接受,但在多人协作、持续集成或长期维护场景中,我们就会遇到项目的构建流程不可复现、项目的成员之间使用的工具链难以统一、新成员难以上手等棘手的问题。为了解决这些问题,Python 社区逐步形成了一套围绕 PEP 517 / PEP 518 / PEP 621 的现代打包规范,其核心思想是:
将项目的元数据、构建方式与依赖声明集中到一个统一的配置入口中。
这个入口贯彻到如今具体的项目实践中就是 pyproject.toml 文件。换言之,如果想要顺利地完成项目的打包,我们首先需要确保 pyproject.toml 文件中包含了正确的配置。在上一节中,我们已经为读者介绍了如何在 pyproject.toml 文件中使用[project]配置项声明当前项目的基本元数据(包括项目的直接依赖列表)。接下来,让我们将目光转向与项目打包相关的[build-system]配置项,它用于声明项目打包所要使用的工具,先来看一个简单的示例:
[project] # 项目元信息配置声明
name = "python-demo"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
requires-python = ">=3.13"
dependencies = [
"flask>=3.1.0",
]
[build-system] # 项目打包方式配置声明
requires = ["hatchling"]
build-backend = "hatchling.build"
在上述文件的[build-system]部分,我们做了如下配置:
requires列表声明了打包项目所依赖的构建工具;build-backend声明了打包项目时需要调用的后端工具;
在这里,hatchling 是一个常用的构建后端工具,uv 会根据 build-backend 的配置来调用相应的底层后端工具来完成项目的打包。当然,uv 并不强制用户使用某一特定的底层后端工具,读者在这里也可以求选择使用 setuptools、flit、poetry 等来充当底层的后端工具,只需要将上述文件中的[build-system]部分修改为如下内容即可:
requires = ["setuptools>=58.0.4"]
build-backend = "setuptools.build_meta"
# 或者:
requires = ["flit_core>=3.2.0"]
build-backend = "flit_core.build_backend"
# 或者:
requires = ["poetry-core>=1.0.0"]
build-backend = "poetry.core.masonry.api"
在完成配置之后,我们就可以通过执行 uv build 命令将项目打包成 Python 社区通用的扩展包文件(扩展名为 .whl 的 wheel 文件,以及扩展名为 .tar.gz 的源码包文件)。在执行该命令的过程中,uv 会依次执行以下步骤:
- 读取
pyproject.toml; - 调用指定的底层后端工具;
- 在项目根目录下生成
dist目录; - 将项目打包的结果输出到其中。
例如具体到python_demo项目中,在我们执行了 uv build 命令后,就可以在项目根目录下的 dist 目录中分别看到名为 python_demo-0.1.0-py3-none-any.whl 的 wheel 文件,和名为 python_demo-0.1.0.tar.gz 的源码包文件,如图 5 所示:

图 5: 使用 uv 打包项目
接下来,我们就可以将这些打包的结果上传至私有或公共包仓库、又或者直接分享给其他人,让他们通过 uv pip install 命令以自定义扩展包的形式安装到自己所在的 Python 运行环境中。例如在这里,当我们执行 uv pip install dist/python_demo-0.1.0-py3-none-any.whl 命令时,uv 就会自动调用 pip 解析出该扩展包的元数据与依赖关系,并按照其中声明的版本号来安装它,如图 6 所示:

图 6: 使用 pip 安装扩展包
如读者所见,在执行完安装命令之后,我们就可以在当前虚拟环境中运行的 Python Shell 中使用 import 语句来导入并使用该扩展包了。
使用 uv 来构建 CLI
除了将项目打包成可供他人使用的扩展包之外,我们更多时候还需要将 Python 项目分发成命令行工具(CLI)的形式,直接提供给用户使用。例如,如果我们现在想将之前的 python_demo 项目实现成一个基于Flask 框架的 Web 服务应用。那么,首先,我们需要将项目根目录下的src/python_demo/main.py 文件修改为如下内容:
from flask import Flask
app = Flask(__name__)
@app.route("/")
def hello():
return "Hello, World!"
def main() -> None:
app.run()
然后,我们就只需要想办法将该项目打包成 CLI 的方式,让用户在命令行中直接运行这个 Web 服务即可。为了实现这个目标,我们需要在 pyproject.toml 文件中为这个项目配置一个 CLI 入口点,这可以通过 [project.scripts] 配置项来完成,下面是一个简单的演示:
[project] # 项目元信息配置声明
name = "python-demo"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
requires-python = ">=3.13"
dependencies = [
"flask>=3.1.0",
]
[build-system] # 项目打包方式配置声明
requires = ["hatchling"]
build-backend = "hatchling.build"
[project.scripts] # CLI 入口点配置声明
web-hello = "python_demo.main:main"
在上述配置文件中,[project.scripts] 配置项下面包含了以下信息:
- 在安装了该项目的分发包之后,当前 Python 运行环境中会生成一个名为
web-hello的命令; - 当用户在当前 Python 运行环境中执行
web-hello命令时,Python 解释器会自动调用python_demo.main:main方法。
在完成上述配置之后,我们就可以通过执行 uv build 命令将项目打包成 CLI 的形式,并使用 uv pip install 命令安装它。在安装完成之后,我们就可以在当前 Python 运行环境中执行 web-hello 命令来启动这个 Web 服务了,如图 7 所示:

图 7: 使用 uv 构建 CLI
需要注意的是,由于在某些面向非开发者的场景中,要求用户事先安装 Python 并配置环境并不具备现实的可操作性。这时,我们确实可以适当考虑将项目打包为独立可执行文件。但这类方案通常会存在诸如项目构建体积较大、构建流程更复杂、与平台强绑定等问题,因此在实际项目中并不推荐作为默认选择。
从工程角度理解“打包”的意义
将项目打包,并不仅仅是为了发布,更重要的是它带来的工程约束:
- 项目必须拥有清晰的入口;
- 元数据必须完整且一致;
- 构建过程必须是可重复的。
这些约束反过来,会倒逼项目在设计阶段就保持良好的结构与边界。
本篇小结
在这一篇笔记中,我们完成了从“会写 Python 代码”到“能交付 Python 项目”的过渡:
-
通过 PEP 规范约束项目结构与编码风格;
-
通过虚拟环境与 uv 管理依赖;
-
通过标准化打包流程实现可复现的构建与分发。
从这一刻开始,Python 不再只是脚本语言,而成为了一种可以被工程化、产品化的开发工具。
浙公网安备 33010602011771号