(二)包的开发 | Word Count: 2k | Reading Time: 7mins | Post Views:
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_amodule_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.py
和 pyproject.toml
必须存在一个,用于说明包如何安装,后者更加先进,建议使用。
模块的初始化
__init__.py
是 Python 包的初始化模块,它负责定义包在被导入时的行为。
包的标识 :没有 __init__.py
的文件夹将不会被视为 Python 包(在较早的 Python 版本中这是必须的,但在 Python 3.3 及之后不是必须的了,尽管还是一个好习惯)。
导入时行为 :当你直接导入包时,例如 import my_package
,只有 __init__.py
中定义的内容(函数、类、变量)可以被直接使用。要使用其他子模块或子包中的内容,需要显式导入它们,或在 __init__.py
中设置好默认的导入。当包被导入时,__init__.py
中的代码会被自动执行一遍。因此,如果有初始化逻辑(例如设置某些配置、加载资源),可以在 __init__.py
中编写,它们会在导入包时执行。但 __init__.py
不会作为脚本运行(即不会执行 __main__
语句中的内容)。
导入的顺序 :深度优先 算法。先确保 父包初始化,在父包的初始化过程中,按照导入顺序逐步进行深度优先的子包和子模块初始化。
__main__.py
文件的作用是在 Python 包被直接执行时,定义程序的入口点。它的功能类似于脚本的主程序入口,让包像脚本一样直接运行。python -m my_package
开发测试
本地测试的时候,应该在包内使用相对路径,并且以“可编辑模式”安装到虚拟环境中。这会在虚拟环境或全局环境中创建一个符号链接,指向包的源代码目录,修改源代码会立即生效。
单元测试:
1 2 3 4 5 6 7 8 9 10 11 import unittestfrom my_package.module1 import some_functionclass 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] include = ["sgp" ][tool.poetry.include] README.md = { path = "README.md" , format = "text/markdown" }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.whl
和 pip install dist/my_package-0.1.0.tar.gz
安装源代码分发包时,pip 会从源代码构建包,这通常涉及到编译步骤。如果项目中有 C 扩展或其他需要编译的代码,安装时会需要构建工具(如 gcc、make 等)来编译这些部分。这种方式兼容性好。但是构建时如果出问题,需要使用者具备比较深入的知识。如果你的项目包含 C 扩展或其他需要编译的代码,源代码分发包是必不可少的。
.whl
文件是一种标准的 Python 二进制包格式,它是预编译好的包。包含编译后的文件,并且已经处理好所有依赖,因此安装时不需要再次编译。Wheel 包通常包括预编译的 C 扩展、二进制文件以及纯 Python 文件。安装速度快,不用编译,但是是平台和架构相关的,需要编译多个版本。
发布的时候,使用 twine upload dist/*
。