Python项目工程化实践:从虚拟环境到CI/CD的完整开发流程
2026/8/13 3:10:33 网站建设 项目流程

1. 项目概述:从零构建一个健壮的Python项目

最近几年,Python的热度居高不下,无论是数据分析、自动化脚本、Web开发还是人工智能,它几乎无处不在。但很多朋友,尤其是刚入门的新手,常常会陷入一个误区:以为学会了语法,写几个脚本,就算掌握了Python。实际上,从写一个孤立的脚本到构建一个结构清晰、易于维护、可协作的“项目”,中间隔着一道鸿沟。我见过太多人写的代码,所有东西都堆在一个.py文件里,没有版本控制,依赖混乱,换台电脑就跑不起来。这就像盖房子,砖头水泥都有,但没图纸、没规划,最后只能得到一个摇摇欲坠的棚子。

今天,我就以一个从业者的角度,和你从头到尾拆解如何搭建一个“超详细”的Python项目。这个“详细”,不是指代码行数多,而是指项目的骨架清晰、工具链完整、开发流程规范。我们会从最基础的目录结构开始,一步步引入虚拟环境、依赖管理、代码风格、单元测试、文档编写,直到最终的打包分发。无论你是想开发一个爬虫工具、一个数据分析包,还是一个Web应用后端,这套方法论都是通用的。我们的目标不是写一个能运行的脚本,而是打造一个经得起时间考验、能让别人(包括三个月后的你自己)轻松接手的工程化项目。

2. 项目骨架:目录结构与核心文件设计

一个项目的起点,不是打开编辑器就写代码,而是先搭好架子。合理的目录结构是项目可维护性的基石。

2.1 标准项目目录解析

我推荐一个经过多年实践检验的目录结构,它平衡了简单与扩展性。假设我们的项目叫做my_awesome_project

my_awesome_project/ ├── .git/ # Git版本控制目录(自动生成) ├── .gitignore # Git忽略文件配置 ├── .venv/ # 虚拟环境目录(通常被.gitignore忽略) ├── docs/ # 项目文档目录 │ ├── conf.py # Sphinx文档配置 │ └── index.rst # 文档首页 ├── src/ # 源代码主目录(核心!) │ └── my_awesome_project/ # 以项目名命名的包目录 │ ├── __init__.py # 包初始化文件 │ ├── core.py # 核心业务逻辑 │ ├── utils.py # 工具函数 │ └── submodule/ # 子模块 │ └── __init__.py ├── tests/ # 单元测试目录 │ ├── __init__.py │ ├── test_core.py │ └── test_utils.py ├── scripts/ # 可执行脚本目录 │ └── cli.py # 命令行入口 ├── data/ # 数据文件目录(示例、输入输出) │ ├── input/ │ └── output/ ├── .env.example # 环境变量示例文件 ├── .pre-commit-config.yaml # Git提交前钩子配置 ├── pyproject.toml # 现代项目配置核心(依赖、构建、工具) ├── README.md # 项目总览文档 ├── LICENSE # 开源许可证 └── CHANGELOG.md # 版本变更日志

为什么要把源码放在src/目录下,而不是直接在项目根目录?这是一个关键设计。这被称为src-layout。它的主要好处是强制隔离,避免在导入时混淆项目代码和测试代码,也防止你无意中从当前目录(而非安装的包)导入模块,这在打包时尤其重要。

2.2 核心配置文件:pyproject.toml的现代之道

过去,Python项目依赖setup.pyrequirements.txtsetup.cfgMANIFEST.in等多个文件,非常混乱。现在,pyproject.toml已经成为事实标准(PEP 518, 621),它用一个文件统一管理几乎所有配置。

一个基础的pyproject.toml应该包含以下部分:

[build-system] requires = ["setuptools>=61.0", "wheel"] build-backend = "setuptools.build_meta" [project] name = "my-awesome-project" version = "0.1.0" description = "一个超详细的示例Python项目" readme = "README.md" license = {text = "MIT"} authors = [{name = "Your Name", email = "you@example.com"}] classifiers = [ "Programming Language :: Python :: 3", "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", ] requires-python = ">=3.8" dependencies = [ "requests>=2.28.0", # 明确的依赖声明 "pandas>=1.5.0", # 更多依赖... ] [project.optional-dependencies] dev = [ # 开发依赖分组 "pytest>=7.0.0", "black>=23.0.0", "isort>=5.12.0", "flake8>=6.0.0", "pre-commit>=3.0.0", "sphinx>=6.0.0", ] test = ["pytest>=7.0.0"] # 测试依赖分组 [project.scripts] my-cli = "my_awesome_project.cli:main" # 定义命令行工具入口 [tool.black] # 代码格式化工具Black配置 line-length = 88 target-version = ['py38'] [tool.isort] # 导入排序工具isort配置 profile = "black" line_length = 88 [tool.pytest.ini_options] # Pytest测试框架配置 testpaths = ["tests"] python_files = "test_*.py" addopts = "-v --tb=short"

这个文件定义了项目的元数据、运行时依赖、分组依赖(开发、测试)、命令行入口,以及各种开发工具的配置。把所有配置集中在这里,让项目一目了然。

注意pyproject.toml中的dependencies列表是项目运行所必需的库。而像pytestblack这类只在开发时用的工具,应该放在[project.optional-dependencies]dev分组里。安装时可以用pip install -e .[dev]来同时安装项目和开发依赖。

3. 开发环境隔离:虚拟环境与依赖管理

“在我电脑上好好的,怎么到你那就报错了?”——这句话的罪魁祸首往往是环境不一致。虚拟环境是解决这个问题的银弹。

3.1 虚拟环境创建与管理

Python 3.3+ 自带了venv模块,这是最标准的选择。在项目根目录下执行:

# 创建虚拟环境,目录名为 .venv(通常加入.gitignore) python -m venv .venv # 激活虚拟环境(Linux/macOS) source .venv/bin/activate # 激活虚拟环境(Windows PowerShell) .venv\Scripts\Activate.ps1 # 或者 Windows CMD .venv\Scripts\activate.bat

激活后,你的命令行提示符通常会变化,显示环境名。之后所有pip install操作都只影响这个隔离的环境。

为什么不推荐condavirtualenv?对于纯Python项目,venv足够轻量且无需额外安装,是Python标准库的一部分,兼容性最好。conda更适合数据科学领域,需要管理非Python依赖(如C库)。virtualenvvenv的前身,现在已无必要使用。

3.2 依赖的精确安装与锁定

有了虚拟环境,我们根据pyproject.toml安装依赖。使用-e参数以“可编辑”模式安装项目本身,这样对src/下的代码修改能立即生效,无需重新安装。

# 安装项目本身及其运行时依赖 pip install -e . # 安装项目及所有开发依赖 pip install -e .[dev]

但是,pip默认安装的是符合版本范围的最新版,这可能导致不同时间、不同人安装的依赖小版本号不同,依然可能引入细微差异。为了绝对一致,我们需要“锁定”依赖版本。

这就是pip-tools的用武之地。首先,在dev依赖组里加入pip-tools。然后,创建一个requirements.in文件,里面只写顶级依赖(就像pyproject.toml里的dependencies)。

# requirements.in requests>=2.28.0 pandas>=1.5.0

接着,运行命令编译出锁定的requirements.txt

pip-compile requirements.in --output-file=requirements.txt

生成的requirements.txt会包含所有顶级依赖及其传递依赖的精确版本号(例如requests==2.28.2)。把这个文件纳入版本控制。任何人在新环境里都可以用pip install -r requirements.txt来复现完全一致的依赖树。对于开发依赖,可以同样创建requirements-dev.in并编译。

实操心得:我习惯将requirements.txtrequirements-dev.txt都纳入Git管理。虽然pyproject.toml是声明依赖的首选,但锁定的txt文件保证了构建的可重复性,特别是在CI/CD(持续集成/部署)流水线中。记得在README.md中说明,开发时使用pip install -e .[dev],而部署或需要精确复现时使用pip install -r requirements.txt

4. 代码质量守护:格式化、检查与Git钩子

代码不仅是给机器执行的,更是给人阅读的。一致的风格和良好的规范能极大提升团队协作效率和代码可维护性。

4.1 自动化代码格式化与检查

我们配置三个工具,分别负责不同方面:

  1. Black:毫不妥协的代码格式化器。你不需要争论缩进是4空格还是2空格,尾随逗号要不要加,Black说了算。它提供统一的代码风格。
  2. isort:自动整理import语句,分组排序,让导入部分整洁清晰。
  3. Flake8:静态代码检查工具,捕捉语法错误、未定义变量、风格问题(如行太长、变量名不规范)等。

它们的配置已经写在了前面的pyproject.toml[tool.*]部分。使用方式如下:

# 使用Black格式化所有Python代码 black src/ tests/ scripts/ # 使用isort整理所有导入语句 isort src/ tests/ scripts/ # 使用Flake8检查代码 flake8 src/ tests/ scripts/

你可以手动运行这些命令,但更好的方法是让它们在提交代码前自动执行。

4.2 使用pre-commit实现提交前自动检查

pre-commit是一个管理Git钩子的框架。在项目根目录创建.pre-commit-config.yaml文件:

repos: - repo: https://github.com/pre-commit/pre-commit-hooks rev: v4.4.0 hooks: - id: trailing-whitespace # 删除行尾空格 - id: end-of-file-fixer # 确保文件以换行符结束 - id: check-yaml # 检查YAML语法 - id: check-added-large-files # 防止提交大文件 - repo: https://github.com/psf/black rev: 23.1.0 hooks: - id: black # 指定格式化目录,与pyproject.toml配置一致 args: [--config=./pyproject.toml] - repo: https://github.com/pycqa/isort rev: 5.12.0 hooks: - id: isort args: ["--profile", "black", "--filter-files"] - repo: https://github.com/pycqa/flake8 rev: 6.0.0 hooks: - id: flake8 args: [--config=./.flake8] # 可额外配置.flake8文件

然后安装并启用pre-commit

# 安装pre-commit钩子到.git目录 pre-commit install # 此后,每次执行`git commit`,这些钩子都会按顺序运行。 # 你也可以手动对所有文件运行一次: pre-commit run --all-files

当你的代码不符合规范时,钩子会拒绝本次提交并指出问题。这强制保证了代码库风格的统一。

踩过的坑:初期团队可能觉得Black的强制格式化很烦人,特别是它会把很长的字符串字面量拆成多行。但坚持一段时间后,你会发现它彻底消除了代码风格的争论,把精力从“代码怎么写好看”解放到“代码逻辑怎么设计”上。对于已有的老项目,可以先用black --check只检查不修改,逐步推进。

5. 测试驱动:编写可靠的单元测试

没有测试的项目就像没有安全网的走钢丝。测试不仅能发现bug,更能作为代码行为的活文档,并支撑安全的代码重构。

5.1 使用pytest框架组织测试

pytest是目前最主流、最强大的Python测试框架。它语法简洁,功能丰富。我们的测试代码放在tests/目录下,文件以test_开头。

假设我们src/my_awesome_project/core.py里有一个计算价格的函数:

# src/my_awesome_project/core.py def calculate_discounted_price(original_price: float, discount_rate: float) -> float: """计算折后价格。折扣率应在0到1之间。""" if not 0 <= discount_rate <= 1: raise ValueError("折扣率必须在0到1之间") if original_price < 0: raise ValueError("原价不能为负数") return original_price * (1 - discount_rate)

对应的测试文件tests/test_core.py可以这样写:

# tests/test_core.py import pytest from my_awesome_project.core import calculate_discounted_price def test_calculate_discounted_price_normal(): """测试正常情况下的折扣计算""" assert calculate_discounted_price(100.0, 0.2) == 80.0 assert calculate_discounted_price(50.0, 0.0) == 50.0 # 无折扣 assert calculate_discounted_price(30.0, 1.0) == 0.0 # 免费 def test_calculate_discounted_price_with_float_precision(): """测试浮点数精度处理(使用pytest的近似相等断言)""" result = calculate_discounted_price(100.0, 0.33) assert result == pytest.approx(67.0) # 近似相等,避免浮点误差 def test_calculate_discounted_price_invalid_discount(): """测试无效折扣率应抛出异常""" with pytest.raises(ValueError, match="折扣率必须在0到1之间"): calculate_discounted_price(100.0, 1.5) with pytest.raises(ValueError): calculate_discounted_price(100.0, -0.1) def test_calculate_discounted_price_invalid_price(): """测试无效原价应抛出异常""" with pytest.raises(ValueError, match="原价不能为负数"): calculate_discounted_price(-10.0, 0.1) # 使用参数化测试,避免写多个重复的测试函数 @pytest.mark.parametrize( "price, discount, expected", [ (100, 0.1, 90), (200, 0.25, 150), (0, 0.5, 0), # 边界情况:原价为0 ], ) def test_calculate_discounted_price_parametrized(price, discount, expected): """使用参数化测试多组数据""" assert calculate_discounted_price(price, discount) == expected

运行测试非常简单:

# 运行所有测试 pytest # 运行特定目录下的测试 pytest tests/ # 运行特定文件中的测试 pytest tests/test_core.py # 运行包含某个字符串的测试函数 pytest -k "discount" # 输出详细结果 pytest -v # 如果测试失败,显示局部变量信息 pytest -v --tb=short

5.2 测试覆盖率报告

知道测试通过了很重要,但知道有多少代码被测试覆盖了更重要。我们可以使用pytest-cov插件来生成覆盖率报告。

首先,将pytest-cov加入pyproject.tomldev依赖。然后运行:

# 运行测试并生成终端覆盖率报告 pytest --cov=src/my_awesome_project tests/ # 生成HTML格式的详细覆盖率报告,便于在浏览器中查看哪行代码没测到 pytest --cov=src/my_awesome_project --cov-report=html tests/

这会在项目根目录生成一个htmlcov文件夹,打开里面的index.html,你可以清晰地看到每个文件的代码覆盖率,以及哪些行是“漏网之鱼”。

注意事项:不要盲目追求100%的覆盖率。关键业务逻辑、复杂的条件分支、错误处理路径应该重点覆盖。对于一些简单的getter/setter或者纯数据类,覆盖率低一些是可以接受的。覆盖率是一个指导工具,而不是终极目标。我通常要求核心模块覆盖率在80%以上。

6. 文档即代码:使用Sphinx生成专业文档

好的文档能极大降低项目的使用门槛和维护成本。我们采用“文档即代码”的理念,将文档和源码一起维护。

6.1 初始化Sphinx文档项目

在项目根目录下,执行以下命令初始化docs/目录:

# 确保已安装sphinx(在dev依赖中) cd docs sphinx-quickstart

在交互式问答中,你可以大部分选择默认值,但注意:

  • 项目名称和作者与pyproject.toml保持一致。
  • 建议将autodoc(自动从代码提取文档)、intersphinx(链接到其他项目文档)等功能都开启。
  • 文档格式选择reStructuredText(.rst),这是Sphinx的默认格式,功能强大。

初始化后,docs/目录下会生成conf.py(配置文件)、index.rst(文档首页)等文件。

6.2 配置自动生成API文档

Sphinx最强大的功能之一是autodoc,它能直接从你的代码和文档字符串(docstring)生成API文档。首先,需要修改docs/conf.py文件,确保Python能找到你的源码:

# docs/conf.py import os import sys sys.path.insert(0, os.path.abspath('..')) # 将项目根目录加入Python路径 # 其他配置... extensions = [ 'sphinx.ext.autodoc', # 核心:自动生成文档 'sphinx.ext.napoleon', # 支持Google/NumPy风格的docstring 'sphinx.ext.viewcode', # 添加指向源代码的链接 'sphinx.ext.intersphinx', ] # Napoleon设置,用于解析我们的docstring napoleon_google_docstring = True napoleon_numpy_docstring = False napoleon_include_init_with_doc = True

然后,在index.rst或其他.rst文件中,使用automodule指令来引入你的模块:

API参考 ======= 核心模块 -------- .. automodule:: my_awesome_project.core :members: :undoc-members: :show-inheritance: 工具模块 -------- .. automodule:: my_awesome_project.utils :members: :undoc-members:

6.3 编写高质量的文档字符串(Docstring)

autodoc依赖你代码中的文档字符串。我强烈推荐使用Google风格的docstring,它清晰易读,且被napoleon扩展完美支持。

def calculate_discounted_price(original_price: float, discount_rate: float) -> float: """ 根据原价和折扣率计算折后价格。 此函数会进行基本的输入验证,确保价格和折扣率在合理范围内。 Args: original_price: 商品的原价。必须为非负数。 discount_rate: 折扣率,范围应在0到1之间(例如,0.2代表8折)。 Returns: 计算得出的折后价格。 Raises: ValueError: 如果 `original_price` 为负数,或 `discount_rate` 不在 [0, 1] 区间内。 Examples: >>> calculate_discounted_price(100.0, 0.2) 80.0 >>> calculate_discounted_price(50.0, 0.0) 50.0 """ # ... 函数实现 ...

写好docstring后,在docs/目录下运行make html(Linux/macOS)或.\make.bat html(Windows),Sphinx就会解析你的代码和rst文件,在_build/html目录下生成精美的HTML文档。你可以将这个目录部署到GitHub Pages或任何静态网站托管服务。

实操心得:将文档生成步骤集成到CI/CD流程中是个好习惯。例如,可以在每次向主分支推送时,自动构建文档并发布。这样你的在线文档永远是最新的。另外,在README.md中放一个“在线文档”的徽章链接,能极大提升项目的专业度。

7. 打包与发布:将你的项目分享给世界

当项目开发成熟,你可能想把它打包成库,通过pip install分享给他人,或者发布到PyPI(Python包索引)。

7.1 使用setuptools与build进行打包

现代Python打包主要依赖setuptoolsbuild工具。我们已经在前面的pyproject.toml[build-system]部分声明了它们。

首先,确保你的pyproject.toml[project]部分信息完整(名称、版本、作者、描述、依赖等)。然后,在项目根目录运行:

# 安装构建工具 pip install build # 执行构建,这会生成源码包(sdist)和轮子包(wheel) python -m build

命令执行成功后,会在项目根目录下生成一个dist/文件夹,里面包含.tar.gz源码包和.whl轮子文件。轮子文件是预编译的二进制分发格式,安装速度更快,是当前推荐的分发格式。

7.2 版本管理与CHANGELOG

在发布前,管理好版本号至关重要。我推荐使用语义化版本控制(SemVer),格式为主版本号.次版本号.修订号MAJOR.MINOR.PATCH):

  • 主版本号:当你做了不兼容的 API 修改。
  • 次版本号:当你做了向下兼容的功能性新增。
  • 修订号:当你做了向下兼容的问题修正。

版本号应直接更新在pyproject.tomlversion字段中。同时,维护一个CHANGELOG.md文件,记录每个版本的变更。格式可以参考 Keep a Changelog 。

# 更新日志 ## [0.1.0] - 2023-10-27 ### 新增 - 实现了核心的 `calculate_discounted_price` 函数。 - 添加了完整的单元测试套件,覆盖率超过90%。 - 配置了Black、isort、Flake8等代码质量工具。 - 使用Sphinx搭建了项目文档框架。 ### 修复 - 无 ### 变更 - 无

7.3 发布到PyPI(可选)

如果你想让全世界都能通过pip install your-project安装你的包,可以发布到PyPI。首先,你需要去 PyPI官网 注册账号。然后,安装发布工具twine

pip install twine

使用twine上传你在dist/目录下生成的包文件:

# 上传到测试PyPI(TestPyPI),先进行试发布 twine upload --repository-url https://test.pypi.org/legacy/ dist/* # 如果测试没问题,上传到正式的PyPI twine upload dist/*

系统会提示你输入PyPI的用户名和密码。为了安全,建议使用API令牌代替密码。

重要警告:PyPI上的包名是全局唯一的,且一旦发布某个版本,你就不能修改或删除它(只能标记为隐藏)。因此,在正式发布前,务必在TestPyPI上充分测试。同时,确保你的包名在PyPI上尚未被占用。

8. 进阶配置与工作流集成

一个成熟的项目,往往还需要一些进阶配置来提升开发体验和自动化水平。

8.1 使用Makefile或Justfile统一命令

项目根目录下会有很多命令:运行测试、格式化代码、构建文档、打包等等。我们可以用一个工具来统一管理这些命令。在Unix-like系统上,经典的Makefile是不错的选择;跨平台的话,just(一个命令运行器)是更现代的选择。

这里展示一个简单的Makefile

.PHONY: help install-dev format lint test test-cov docs clean build help: ## 显示此帮助信息 @awk 'BEGIN {FS = ":.*?## "} /^[a-zA-Z_-]+:.*?## / {printf "\033[36m%-20s\033[0m %s\n", $$1, $$2}' $(MAKEFILE_LIST) install-dev: ## 安装开发环境依赖(包含项目本身) pip install -e .[dev] format: ## 使用Black和isort格式化代码 black src/ tests/ scripts/ isort src/ tests/ scripts/ lint: ## 使用Flake8进行代码检查 flake8 src/ tests/ scripts/ test: ## 运行测试 pytest -v tests/ test-cov: ## 运行测试并生成覆盖率报告 pytest --cov=src/my_awesome_project --cov-report=term-missing --cov-report=html tests/ docs: ## 构建Sphinx文档 cd docs && make html clean: ## 清理构建产物和缓存文件 rm -rf build/ dist/ *.egg-info .pytest_cache .coverage htmlcov/ find . -type d -name __pycache__ -exec rm -rf {} + find . -type f -name "*.pyc" -delete build: clean ## 清理并构建分发包 python -m build

这样,开发者只需要记住make testmake format等简单命令即可。

8.2 集成持续集成/持续部署(CI/CD)

将你的项目托管在GitHub、GitLab等平台后,可以配置CI/CD流水线,自动化执行测试、代码检查、构建和部署。这里以GitHub Actions为例,在项目根目录创建.github/workflows/ci.yml

name: CI on: [push, pull_request] jobs: test: runs-on: ubuntu-latest strategy: matrix: python-version: ["3.8", "3.9", "3.10", "3.11"] # 测试多个Python版本 steps: - uses: actions/checkout@v3 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} - name: Install dependencies run: | python -m pip install --upgrade pip pip install -e .[dev] - name: Lint with flake8 run: | flake8 src/ tests/ scripts/ - name: Test with pytest run: | pytest --cov=src/my_awesome_project tests/

这个工作流会在每次推送代码或创建拉取请求时,在多个Python版本下自动安装依赖、运行代码检查、执行测试并计算覆盖率。这能及时发现问题,保证代码库的健康。

9. 常见问题与排查技巧实录

即使按照最佳实践搭建项目,在实际操作中还是会遇到各种问题。这里记录几个我踩过的坑和解决方法。

9.1 导入错误:ModuleNotFoundError

问题:在项目根目录直接运行python src/my_awesome_project/core.py报错ModuleNotFoundError: No module named 'my_awesome_project'

原因:Python的模块导入路径问题。当你直接运行一个脚本时,Python会将脚本所在目录加入sys.path。此时src/目录不在路径中,无法找到同级的其他模块。

解决方案

  1. 推荐:总是通过安装后的包名来导入。在开发时,使用pip install -e .将项目以可编辑模式安装到虚拟环境中,然后就可以在任何地方(比如在项目根目录)通过from my_awesome_project.core import ...来导入,或者在命令行直接使用my-cli命令(如果配置了project.scripts)。
  2. 临时方案:如果你必须直接运行某个脚本,可以在脚本开头修改sys.path。但这是一种“坏味道”,不推荐在生产代码中使用。
    # scripts/cli.py 开头 import sys from pathlib import Path sys.path.insert(0, str(Path(__file__).parent.parent / 'src'))

9.2 依赖版本冲突

问题:项目依赖A库版本>=2.0,但同时依赖B库,而B库依赖A库版本<2.0。导致pip install失败。

原因:Python的依赖解析器在遇到无法同时满足的版本约束时会失败。

排查与解决

  1. 查看依赖树:使用pipdeptree工具 (pip install pipdeptree) 查看完整的依赖关系。
    pipdeptree
  2. 寻找替代库:检查冲突的库B是否有更新版本,已经支持A库的新版本。
  3. 向上游报告:如果B库是开源项目且长期未更新,可以考虑在其Issue页面反馈。
  4. 使用依赖版本上限:在你的pyproject.toml中,为A库设置一个与B库兼容的版本上限,例如requests>=2.28,<2.29。但这只是权宜之计。
  5. 终极方案:如果冲突无法调和,考虑是否能用其他功能类似的库替换B库。

9.3 测试通过但实际运行出错

问题pytest全部通过,但手动运行程序或用其他方式调用时出错。

原因:最常见的原因是环境差异。测试环境(pytest)和运行环境(如生产环境、其他脚本)的PYTHONPATH、当前工作目录、环境变量可能不同。

排查步骤

  1. 检查导入路径:在出错的地方打印sys.path,对比测试环境和运行环境。
  2. 检查当前工作目录:打印os.getcwd()
  3. 检查环境变量:特别是那些用于配置的变量。
  4. 模拟真实环境测试:不要只运行单元测试。编写集成测试或使用pytesttmp_path等fixture来模拟文件系统操作。或者,直接写一个小脚本,模拟真实的调用流程来测试。
  5. 使用调试器:在怀疑的代码处设置断点,使用pdb或IDE的调试功能,单步跟踪执行流程,观察变量状态。

9.4 打包时包含/排除了不该有的文件

问题:执行python -m build后,生成的包文件里缺少了数据文件,或者多了一些缓存文件、测试文件。

原因setuptools默认有一套文件包含规则,但可能不符合你的预期。

解决方案:在pyproject.toml中使用[tool.setuptools][tool.setuptools.package-data]进行精细控制。

[tool.setuptools] # 使用find:指令自动发现包,通常配合src-layout无需额外配置 packages = ["my_awesome_project"] # 明确包含非Python文件(数据文件、模板等) [tool.setuptools.package-data] "my_awesome_project" = ["data/*.json", "templates/*.html"] # 或者,使用更灵活的include/exclude模式 [tool.setuptools] include-package-data = true # 通过MANIFEST.in文件进行更复杂的控制(传统方式,但依然有效) # 在项目根目录创建MANIFEST.in文件

一个简单的MANIFEST.in文件示例:

include LICENSE include README.md include CHANGELOG.md recursive-include docs *.rst *.png *.jpg recursive-include data *.csv *.json global-exclude __pycache__ global-exclude *.py[co]

构建后,可以使用tar -tzf dist/*.tar.gzunzip -l dist/*.whl来检查打包内容是否符合预期。

搭建一个规范的Python项目,初期会感觉有些繁琐,但一旦这套流程跑顺,它会像精良的流水线一样,为你后续的开发、协作、维护节省无数的时间和精力。从虚拟环境隔离到依赖锁定,从自动化代码检查到完整的测试覆盖,再到专业的文档和打包发布,每一步都是在为项目的长期健康投资。最开始的“麻烦”,最终都会转化为稳定性和效率的回报。当你需要回头修改半年前写的代码,或者有新成员加入团队时,你会庆幸当初搭建了这样一个“超详细”的工程化项目结构。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询