管理Python版本

推荐使用 https://github.com/pyenv/pyenv 来管理不同版本的python,但是它不支持windows,如果你使用windows,请使用 https://github.com/pyenv-win/pyenv-win

如果你需要使用特定版本的python,先安装它,类似于

1
pyenv install 3.9.6

随后在项目的根目录,使用

1
pyenv local 3.9.6

它会生成一个配置文件 .python-version ,就会让进入项目根目录,就会自动切换到3.9.6的版本。

管理虚拟环境

Poetry 适合现代 Python 项目开发、个人或小型团队开发、需要发布到 PyPI 的项目。

特点

  • 自动化依赖管理:Poetry 能够自动处理依赖冲突,生成 pyproject.toml 和 poetry.lock 文件,确保依赖一致性。
  • 内置虚拟环境管理:Poetry 会自动为每个项目创建虚拟环境,隔离项目依赖,保证项目间的互不干扰。
  • 简化的发布流程:通过 poetry build 和 poetry publish,可以轻松地将 Python 包发布到 PyPI。
  • 现代化设计:Poetry 支持 pyproject.toml,这是 Python 最新的标准配置文件格式。

优点:

  • 统一的依赖管理和虚拟环境管理,操作简单。
  • 自动解决依赖冲突,生成锁定文件,保证不同环境依赖的一致性。
  • 提供项目的创建、构建、依赖管理、测试和发布的全流程工具。
  • pyproject.toml 文件非常清晰,方便配置。
1
2
3
4
5
poetry new my_project      # 创建新项目
poetry add requests # 添加依赖
poetry install # 安装依赖
poetry shell # 进入虚拟环境
poetry run python script.py # 在虚拟环境外,使用虚拟环境运行脚本

Poetry对当前的python解释器有依赖。当你使用 Poetry 创建虚拟环境时,它会检查系统中当前使用的 Python 版本是否符合项目中的版本要求。如果当前系统或环境中的 Python 版本不符合 pyproject.toml 中的规定,Poetry 会提示错误,阻止虚拟环境的创建或依赖安装。报错类似 Poetry could not find a compatible version of python for your project.

为了弥补 Poetry 不能直接管理 Python 版本的缺点,你可以结合 pyenv 这类工具来一起使用。

以上pyenv会自动切换环境,这样就没有问题了。


相比conda,poetry 专注于依赖管理和包管理,适合纯 Python 项目。Conda 创建的虚拟环境是完全隔离的,每个环境可以有不同的 Python 版本和不同的依赖包,管理起来非常方便。

但是Conda在一些科学计算库上,有预编译的版本(如 numpy, scipy, pandas),Conda 可以直接安装预编译好的版本,避免本地编译的复杂性。尤其在windows平台,能解决许多依赖包编译失败的问题


关于macOS(darwin)系统,它确实在用户体验上下了功夫,并且有类unix的系统。但是它的编译工具链,依赖库等生态,还是不如linux,尤其是需要自行编译的部分,比如部分python的包,就会容易遇到各种bug,需要手动解决,再加上arm架构,会进一步造成一些兼容性问题。

包的开发

Python 包的开发是一项重要的技能,可以帮助你组织代码、实现代码复用,并且将功能模块分发给其他开发者使用。

包和模块是什么

在 Python 中,一个包(Package)就是一个包含多个模块的目录,其中通过 __init__.py 文件来表明它是一个包。包允许你将代码逻辑分解为多个文件,并且通过模块的方式进行导入和复用。

  • 模块:一个 Python 文件(*.py 文件)就是一个模块。模块可以包含函数、类和变量。
  • 包:包是包含多个模块的文件夹。包使得模块之间可以被组织起来,以便更好的管理。

目录结构示例:

1
2
3
4
my_package/
├── __init__.py # 包初始化文件
├── module_a.py # 模块 A
├── module_b.py # 模块 B

在代码中使用包(如果要供外部使用,推荐使用相对路径):

1
2
3
4
5
# 导入包中的模块
from my_package import module_a

# 使用模块中的函数
module_a.some_function()

一个典型的python 包的格式

1
2
3
4
5
6
7
8
9
10
11
12
my_package/
├── my_package/ # 包目录
│ ├── __init__.py # 包初始化文件
│ ├── module1.py # 包中的第一个模块
│ └── module2.py # 包中的第二个模块
├── tests/ # 测试目录
│ └── test_module1.py
├── README.md # 包的说明文件
├── setup.py # 安装脚本
├── pyproject.toml # 现代构建系统配置文件
├── LICENSE # 许可证
└── requirements.txt # 依赖文件

setup.pypyproject.toml 必须存在一个,用于说明包如何安装,后者更加先进,建议使用。

开发测试

本地测试的时候,应该在包内使用相对路径,并且以“可编辑模式”安装到虚拟环境中。这会在虚拟环境或全局环境中创建一个符号链接,指向包的源代码目录,修改源代码会立即生效。

1
pip install -e .

单元测试:

1
2
3
4
5
6
7
8
9
10
11
# tests/test_module1.py
import unittest
from my_package.module1 import some_function

class TestModule1(unittest.TestCase):
def test_some_function(self):
result = some_function()
self.assertEqual(result, "expected result")

if __name__ == "__main__":
unittest.main()

还有简单的测试,文件以 _test.py 结尾,然后函数以 test_ 开头,依赖pytest。

安装包

下面是一个传统的pyproject的定义,[build-system] 里这两者一般都不用变动。

  • requires 指定了构建项目所需的工具和版本。在这个例子中,你需要 setuptools 版本 >= 61.0 来进行包的构建。
  • build-backend 指定了构建后端,这里使用的是 setuptools 的构建元数据模块 setuptools.build_meta。

[project]是核心的配置参数

  • requires-python:表明 Python 版本的要求,这里要求 Python >= 3.9。
  • classifiers:这些是 Python 包的元数据,用于描述包的兼容性、用途和许可证等。
  • dependencies:列出了包的依赖项,当其他人安装此包时,这些依赖项也会被自动安装。

[tool.setuptools.packages.find] 告诉 setuptools 如何找到包。这里它会从当前目录中找到所有以 sgp 开头的包。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
[build-system]
requires = ["setuptools>=61.0"]
build-backend = "setuptools.build_meta"

[project]
name = "openzeppelin-solidity-grammar-parser"
version = "0.0.4"
authors = [{ name = "Georgii Plotnikov", email = "accembler@gmail.com" }]
description = "Solidity ANTLR4 grammar Python parser"
readme = "README.md"
requires-python = ">=3.9"
classifiers = [
"Programming Language :: Python :: 3",
"License :: OSI Approved :: MIT License",
"Operating System :: OS Independent",
]
dependencies = [
"antlr4-python3-runtime == 4.13.1",
"coverage == 7.3.1",
"simplejson == 3.19.1",
"typing == 3.7.4.3",
"typing_extensions == 4.8.0",
]

[project.urls]
"Homepage" = "https://github.com/OpenZeppelin/sgp"
"Bug Tracker" = "https://github.com/OpenZeppelin/sgp/issues"

[tool.setuptools.packages.find]
where = ["."]
include = ["sgp*"]

对于poetry工具,需要另外一套配置,则把元信息,运行时依赖依赖和开发时的依赖,构建系统都写的比较清楚。poetry build 的效果和 python -m build 类似,会在 dist/ 目录下生成两种类型的分发文件:

  • Source distribution (sdist):一个 .tar.gz 文件,用于源代码分发。
  • Wheel (bdist_wheel):一个 .whl 文件,这是已编译的、便于安装的包格式。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
[tool.poetry]
name = "openzeppelin-solidity-grammar-parser"
version = "0.0.4"
description = "Solidity ANTLR4 grammar Python parser"
authors = ["Georgii Plotnikov <accembler@gmail.com>"]
readme = "README.md"
license = "MIT"
homepage = "https://github.com/OpenZeppelin/sgp"
repository = "https://github.com/OpenZeppelin/sgp"
documentation = "https://github.com/OpenZeppelin/sgp"
keywords = ["solidity", "parser", "antlr4"]

[tool.poetry.dependencies]
python = ">=3.9"
antlr4-python3-runtime = "4.13.1"
coverage = "7.3.1"
simplejson = "3.19.1"
typing = "3.7.4.3"
typing_extensions = "4.8.0"

[tool.poetry.dev-dependencies]
black = "^23.0"
ruff = "^0.0.288"
pytest = "^7.0"

[tool.poetry.packages]
# 只包含 sgp 目录下的代码
# 这将确保打包时只会包含 sgp 目录,而不会把其他项目文件夹(如 tests/、scripts/ 等)打包进来。
include = ["sgp"]

# 这一部分用于指定额外要包含的非 Python 文件。
[tool.poetry.include]
# 包含 README.md
README.md = { path = "README.md", format = "text/markdown" }
# 包含 LICENSE
LICENSE = { path = "LICENSE", format = "text/plain" }
# 包含某些其他文件
"sgp/parser/*.tokens" = { format = "text/plain" }

[build-system]
requires = ["poetry-core>=1.1.0"]
build-backend = "poetry.core.masonry.api"

而且 poetry install 不仅会安装好依赖,还会执行了类似 pip install -e . 的功能,执行了可编辑模式安装。

代码编译分发

上一小节介绍了如何打包,并且介绍了2种打包生成的文件。。

1
2
3
dist/
my_package-0.1.0.tar.gz
my_package-0.1.0-py3-none-any.whl

这里两种分发包都是可以安装的 pip install dist/my_package-0.1.0-py3-none-any.whlpip install dist/my_package-0.1.0.tar.gz

安装源代码分发包时,pip 会从源代码构建包,这通常涉及到编译步骤。如果项目中有 C 扩展或其他需要编译的代码,安装时会需要构建工具(如 gcc、make 等)来编译这些部分。这种方式兼容性好。但是构建时如果出问题,需要使用者具备比较深入的知识。如果你的项目包含 C 扩展或其他需要编译的代码,源代码分发包是必不可少的。

.whl 文件是一种标准的 Python 二进制包格式,它是预编译好的包。包含编译后的文件,并且已经处理好所有依赖,因此安装时不需要再次编译。Wheel 包通常包括预编译的 C 扩展、二进制文件以及纯 Python 文件。安装速度快,不用编译,但是是平台和架构相关的,需要编译多个版本。
发布的时候,使用 twine upload dist/*