现代 Python 开发指南一文全解

一个现代的项目开发需要包含两个方面,一个是项目管理,另一个是代码质量。Astral 就同时提供了这两个工具: uv 和 ruff,可以一步到位。而且这两个工具都是基于 Rust 开发的,相比其他工具有非常大的性能优势,官方号称快 10-100 倍。

传统项目管理

在平常开发比较小的项目时,通常都不太会依赖项目管理工具,但是一般会使用 pip 和 conda 来管理 Python 环境。对于依赖的环境,一般会使用 pip 的 freeze 把依赖保存到 requirements.txt 中。但是 pip 和 requirements.txt 功能比较简单,需要手动维护,而且有版本冲突的风险。而如果存在不同的环境依赖,例如开发环境和生产环境,就需要有不同的 requirements.txt 文件,这个时候手动管理的复杂度就又要上升了,很容易出错。

而代码质量这方面,我们知道 Python 对于使用的限制非常小,有很多容易出错的代码实现方式。所以社区提出了很多工具来避免容易产生错误的写法,比如 Pylint 提供了非常多的规则来控制 Python 的代码质量,包含了错误、警告和规范三种级别的规则。

除了代码实现方式的质量保证外,还有一部分工具提供了代码格式化的功能,保证格式规范清晰的代码。比如 black 就提供了很多代码规范的设置,比如函数参数过多时,会自动分行、每行代码不能超过 80 列等等规范。这些额外的工具需要个性化配置的话,还需要写额外的配置文件,比如 .pylintrcpytest.ini 等。

项目打包和分发

对于另外一部分需要分发打包的项目,比如写一些库和工具提供给其他人用,这个时候就需要打包发布到公共仓库或私有仓库,打包后的项目可以让其他人轻松地使用 pip 来安装。

在打包时会使用到一系列 build 工具,官方提供了 setuptools,可以将项目打包成源码包和 wheel 包

  • 源码包:直接将原始的源代码整理成标准格式,安装使用这个包时就像正常引入代码一样动态执行;包含 C/C++ 拓展时,需要本地编译,安装速度慢
  • wheel 包:打包成 Python 字节码;或二进制拓展(包含 C/C++ 拓展时),无需本地编译,安装速度快,不过因为是编译过的,所以对兼容性有要求

对于这类项目的管理,传统方法通常使用 setup.py 来管理,这里提供一个简单的示例来直观感受一下:

from setuptools import setup, find_packages

setup(
    name="my_package",
    version="0.1.0",
    author="Your Name",
    author_email="your.email@example.com",
    description="A short description of your package",
    packages=find_packages(),
    python_requires=">=3.6",
    # 依赖项
    install_requires=[
        "requests>=2.25.1",
        "numpy>=1.20.0",
    ]
)

可执行程序

不过最终的分发很可能是构建成可执行程序,因为以上的这些方法的使用场景都偏向开发者,需要用户手动去创建环境,对于普通用户来说使用成本还是太高了。而可执行程序就只要下载运行就可以了,不用管上面那么多。

一个非常常用的工具就是 pyinstaller,可以把整个项目打包成一个单独的可执行文件,但是因为 Python 是动态执行的,所以打包的时候会把 Python 解释器和一系列环境依赖一起打包进去。本质上就是把 Python 及各种环境和项目打包在一起,然后一键执行而已,所以一般文件都会比较大。不是 C/C++ 这种可以直接编译成二进制的语言,文件可以很小。

其他

为了确保更健壮的 Python 项目,通常还要配上一些测试代码,这里又会牵扯到其他测试的工具,比如 unittestpytest。想要实现自动化测试的话,还需要写一些脚本和引入额外的工具。

现代项目管理

以上介绍的传统方法,在各个部分有不同的工具,环境管理用 pip 和 conda、项目打包用 setuptoolssetup.py、可执行程序用 pyinstaller、自动化测试需要自己写脚本。

可以看到不同的部分都要管理不同的文件,要想实现一个健壮的项目,就要管理这么多不同的文件和工具。然而使用的工具和管理的文件多了,产生错误的概率也会上升,所以后来又引入了基于 toml 的项目配置文件来进行统一的管理,这个时候只需要 toml 配置就可以完成以上所有的配置。

toml 介绍

toml 本质上就是一种配置语言,类似 .ini 和 json 这样的简单语法的语言。不过 toml 非常简洁,而且很适合 Python 使用,一是语法相似,二是类型可以一一对应。

一个简单的示例如下:

name = 'tom'
age = 25
[subdict]
name = 'jack'
age = 21

在 Python 中调用就非常简单:

import tomllib
with open("config.toml", "br") as f:
	config = tomllib.load(f)
	# 输出 'tom'
	print(config["name"])
	# 输出 'jack'
	print(config["subdict"]["name"])

其他更复杂的还支持字典和列表,都是和 Python 一一对应的,就不过多赘述了。

基于 toml 的项目管理

通常我们会把项目的各种配置信息写入到 pyproject.toml 文件中,一般包含五种类型的信息:

  • 项目元数据:定义项目名称、版本、作者等等
  • 依赖管理:定义项目依赖的库信息
  • 构建管理:指定构建工具和构建参数
  • 工具配置:指定测试框架、代码格式化工具、代码检查工具
  • 脚本入口:定义一个全局的脚本命令入口

一个典型的 toml 配置如下:

[project]
# 项目元数据
name = "my-package"
version = "0.1.0"
authors = [{name = "Your Name", email = "you@example.com"}]
description = "A useful Python package."

# 依赖管理
dependencies = [
	"requests>=2.25.0"
]

# 不同依赖管理
[project.optional-dependencies]
test = ["pytest>=6.0.0"]
dev = ["black", "flake8"]

# 构建管理
[build-system]
requires = ["setuptools>=62.0.0"]
build-backend = "setuptools.build_meta"

# 工具配置:测试配置
[tool.pytest.ini_options]
testpaths = ["tests"]

# 工具配置:代码格式化
[tool.black]
line_length = 88

# 脚本入口
[project.scripts]
my-command = "my_package.utils:main"

写完这样一个配置文件之后要如何使用呢?

依赖管理

[project]
dependencies = ["requests>=2.25.0"]

[project.optional-dependencies]
test = ["pytest>=6.0.0"]
dev = ["black", "flake8"]

新版本的 pip 和各种 Python 工具链大部分都支持基于 toml 的项目管理,比如 pip install . 就会搜索目录下的 pyproject.toml 文件,并找到依赖进行安装。如果要指定其他的环境,可以使用 pip install ".[dev]" 来指定安装 dev 下的依赖。除了 pip 之外,社区推出的 poetry 也可以很好地管理环境依赖。

构建管理

[build-system]
requires = ["setuptools>=62.0.0"]
build-backend = "setuptools.build_meta"

对于构建工具,Python 提供了 build 包,就可以使用 pyproject.toml 代替 setup.py。首先通过 pip install build 安装 build 包,然后调用 python -m build 就可以自动根据当前目录下的 pyproject.toml 配置执行项目构建,可以配置指定的构建后端。

工具管理

[tool.pytest.ini_options]
testpaths = ["tests"]

[tool.black]
line_length = 88

比如测试工具 pytest 也支持 toml 配置,当执行命令 pytest 时,会自动根据当前目录下的 pyproject.toml 配置执行测试;类似的,black 也支持,在执行 black . 时,就会自动根据配置进行格式化。

All in One

虽然提供了 toml 配置之后可以很方便地统一管理这些配置,但是仍然需要使用到不同的工具,现在就来介绍一下 Astral 提供的工具:

  • uv:包管理和项目管理
  • ruff:代码检查和格式化

uv

uv 中提供了一套的项目管理工具,初始化时,我们只需要运行 uv init <project name> 就会创建一个包含了 git、pyproject.toml 和一个 hello world 文件的初始项目。

然后我们执行 uv run main.py 就会自动创建一个 .venv 虚拟环境,这个虚拟环境是在当前目录下由 uv 管理的,所以我们不需要 conda 这类的环境管理工具,当然你也可以通过其他命令手动创建。run 命令会执行 .venv 中的 Python 解释器,是单独隔离的。

环境管理

uv 支持通过 uv add 和 uv remove 添加和修改项目的依赖关系,类似于 pip install,此时会自动修改 pyproject.toml 中的配置信息。我们也可以通过手动修改 pyproject.toml 来更新项目的依赖。不过为了兼容之前的 requirements.txt,我们也可以通过 uv add -r requirements.txt 来导入依赖。

除了我们手动管理的依赖外,uv 对于环境管理提供了一个 uv.lock 文件,uv 在执行环境依赖相关的命令时会自动更新该文件。该文件提供了一个准确的依赖信息,比如具体的版本号和对应的哈希值,可以在其他环境下安装依赖时提供完全一致的依赖包安装。

当项目存在多个环境时,例如开发环境,我们不需要开发环境中的依赖在最终打包的时候导出,这可以通过 uv add --dev pytest 来添加一个 dev 环境的依赖安装,此时会自动在 pyproject.toml 添加相关的依赖配置文件。

工作区

uv 参考了 Rust 的 Cargo Workspaces,提供了一种多个项目共享依赖的工作区模式。适用于多个子项目存在共享的依赖,但是各个子项目是可以独立进行的,每个项目都有自己的 toml 配置。

一个典型的工作区结构如下:

my_project
├── packages
│   ├── project1
│   │   ├── pyproject.toml
│   │   └── src
│   │       └── bird_feeder
│   │           ├── __init__.py
│   │           └── foo.py
│   └── project2
│       ├── pyproject.toml
│       └── src
│           └── seeds
│               ├── __init__.py
│               └── bar.py
├── pyproject.toml
├── README.md
├── uv.lock
└── src
    └── my_project
        └── main.py

在这样的结构下,子项目共享根目录下的 lockfile,同时 uv lockuv runuv sync 对依赖的管理都是对根目录下的(所有项目共享)。不过可以通过参数 --package 来指定某一个子项目进行依赖操作。

在配置文件中,我们在 [tool.uv.workspace] 中进行设置:

[tool.uv.workspace]
members = ["packages/*"]

当项目存在依赖关系时,例如上例的 my_project 依赖于 project1,可以在根目录的 toml 中这样写:

[project]
dependencies = ["project1"]
[tool.uv.sources]
project1 = { workspace = true }

workspace = true 代表了 project1 的依赖来源于工作区,而不是 PyPI 或其他地方。根目录下的这一设置会在全局生效,在子项目中不必写明,除非想要覆盖根目录的这一依赖来源设置。

需要注意的是,在工作区的设置下,所有子项目都使用同一个 Python 环境,确保了所有依赖都是完全一致的,且只有这样才实现了「共享」,所以要避免依赖冲突的问题。

脚本

uv 可以单独对脚本提供隔离的环境依赖,在 PEP 723 标准中,可以通过在文件头中的注释块中声明依赖,例如:

# /// script
# dependencies = ["requests", "rich"]
# ///

import requests
from rich import print

print(requests.get("https://example.com").text)

随后在执行 uv run example.py 时,会自动创建一个临时的虚拟环境,并安装 rich 依赖。但如果没有在文件头声明,可以通过 uv run --with rich example.py 来指定依赖。虚拟环境会保存在本地,下次执行时就不用安装了。如果想删除临时的虚拟环境,可以执行 uv clean 来删除。

工具

对于一些全局的 Python 工具,比如 pycowsay,在命令行输入 pycowsay 就可以看到一个 ASCII 风格的牛。uv 提供了两种方式运行,第一种是临时运行,第二种是安装运行,二者都提供了独立隔离的虚拟环境来运行。

  • 临时运行:通过 uvx pycowsay 就可以执行这个工具,等价于 uv tool run pycowsay,会创建临时的环境,要删除需要 uv clean
  • 全局安装:通过 uv tool install pycowsay 安装,此时运行就不需要指定 uv 了,直接调用 pycowsay 就可以

构建

在进行构建时,uv 仅作为构建前端,具体构建后端定义在 toml 配置文件的 [build-system] 中,我们执行 uv build 就可以根据配置文件执行构建了。更多构建相关的内容还得参考对应的后端。

ruff

ruff 是一个 All in One 的代码检查和格式化工具,用于代替 Flake8、Black、isort、pydocstyle、pyupgrade、autoflake 等等工具,而且还比这些工具快 10-100 倍。并且还能够兼容 Flake8、isort、Black,超过 800 条内置规则。

我们可以通过 uv 安装 ruff:uv tool install ruff 就可以在全局安装,也可以通过 pip 安装。执行 ruff check 就可以对当前项目下的代码文件进行检查,这个命令有 --fix 参数,可以自动修复一些不影响代码逻辑的 safe error,比如未使用的 import 和变量。当然也可以指定单个文件:ruff check <file path> 来进行代码检查。对于格式化,命令是 ruff format,基本上 ruff 的所有功能就集中在这几个命令里了。

配置

对于个性化的配置要求,我们可以在 pyproject.toml 里进行配置:

[tool.ruff]
line-length = 79

[tool.ruff.lint]
extend-select = ["E501"]

[tool.ruff] 里的是 ruff 的一些全局配置,例如是否自动修复、行最长、包含哪些文件等等。具体我们可以参考官方文档的 Settings,对于 [tool.ruff.lint] 则是一些要生效的额外规则,我们可以在官方文档的 Rules 里找到,比如有一条规则 B033 是针对 sets 的,如果 sets 里出现重复的元素,则会报错。

在代码格式化部分,ruff 使用配置 [tool.ruff.format] 里的参数,具体的配置可以参考 Settings 里的 format 部分。

最后 ruff 实现了 lsp,可以集成到其他代码编辑器中,例如 Neovim,VSCode 等等

下一篇

评论 | 0条评论