☰
Python代码封装成pip可安装包:从脚本到标准库的完整指南
2026/10/2 18:46:34 网站建设 项目流程

你有没有过这种经历:代码写完了,功能也调通了,但换个电脑或者给别人用时,就是跑不起来。要么是到处找脚本文件,要么是依赖的包一个接一个手动安装,装到一半报错,又得排查半天。我以前也这样折腾过,后来才明白一个道理:与其一次次复制粘贴代码、手动装依赖,不如把写好的Python代码整理成标准的Python包,让别人(或者未来的自己)一条pip install命令就能装好。这篇文章就来聊聊,怎么把普通的Python代码封装成可通过 pip 安装的标准库。

这个需求在开发中非常常见,尤其是当你写了一些可以复用的工具函数、内部业务逻辑,或者给团队提供一个公共服务时。封装的意义不只是“能安装”,更重要的是让你的代码有清晰的边界、明确的依赖、规范的版本,别人用起来省心,你自己维护起来也轻松。无论你是刚学Python、还在纠结“为什么 import 报错”的新手,还是已经在写业务代码、想给团队抽一个公共库的老手,这篇文章的内容都可以直接照着操作。我会从原理讲到实操,把踩过的坑一并写出来。

1. 封装的价值:从“能用脚本”到“好用的库”

1.1 先厘清一个概念:这里的“标准库”指什么

一说到标准库,很多人第一反应是Python自带的os、sys、json这些内置模块。咱们这里说的不是把代码塞进Python解释器里,而是把你的代码打包成一个“分发包”(distribution package),放进PyPI生态里,让其他人可以通过pip install 你的包名来安装,装完以后import 你的包名就能直接用。在Python社区里,这种能被pip管理、遵循PEP 517/518构建规范的包,就是“标准”的第三方库。

说白了,标准库的“标准”二字,指的是打包和发布的规范,而不是指代码本身有多官方。只要遵循这些规范,一个个人写的工具库和numpy、requests在安装机制上没有本质区别,都是通过pip下载、安装、导入。这也是我建议大家尽量走标准封装路线的原因:不搞特殊化,才不会被特殊问题卡住。一个常见的误解是“我的代码很简单,不值得打包”,但事实上,哪怕只有一个模块、几个函数,只要符合 PyPI 的打包规范,它就是一个合格的标准包,而且它带来的工程化收益,和项目规模的关系并不大。

1.2 封装成库之后,你到底获得了什么

封装之后最直接的好处是安装体验的变化。以前你把一个工具函数发给同事,可能要连代码文件带依赖清单一起打包,还得写一段“先装这个再装那个”的说明;封装以后,一条pip install your-lib就完事,依赖由 pip 自动解析安装,使用者根本不用关心你有几个依赖、版本号怎么定。

第二个好处是版本管理。你的库有明确的__version__,用户能看到自己装的是哪个版本,升级时pip install -U一下就行,不用再担心“拿到的代码是几天前的老版本”。这一点在团队协作里尤其重要,因为别人拿到的始终是“当前最新且可安装”的版本,而不是某个聊天记录里传过来的一份快照。

第三个好处是代码结构。为了能打包,你被迫把原本挤在一堆的脚本拆成模块,划分公共接口和内部实现。这个过程本身就是在重构,代码会变得更清晰、更好维护,很多以前靠注释说明的逻辑,现在直接由一个函数或一个模块的边界来表达。第四个好处是自动化部署。在Docker镜像、CI流水线里,只要在 requirements 里加上你的包名,构建环境就能自动装好你需要的代码,不再需要手动把脚本塞进镜像。以前我在容器里部署代码,最烦的就是把脚本和依赖一层层拷进去,封装成库之后,Dockerfile 里一行 pip install 就解决了。

1.3 什么样的代码适合封装成库

不是所有代码都值得封装。我的经验是,三类代码最适合封装成库:一是跨项目复用的工具函数,比如日期处理、加密、格式转换这类通用功能;二是团队内部的公共业务模块,比如统一的日志配置、数据库连接、权限校验逻辑;三是你的开源项目,希望别人也能用起来。反过来,跟具体业务强耦合的页面逻辑、写死在脚本里的配置数据、一次性调度的任务代码,这些不建议硬封装。硬封装的代价是你要维护抽象接口,成本可能比复用收益还高。所以动手之前先想清楚:这段代码会不会被第二个项目用到?如果答案是不会,那封装这事儿可以缓一缓。

还有一个更实际的问题:代码改到多完善才值得封装?我的建议是,不用等到功能完全齐备,只要核心功能稳定、接口设计合理,就可以先封装出来,后面在版本迭代中慢慢完善。因为封装行为的价值在于给了你一个清晰的发布边界,让你能把代码当作正式产品来对待,而不是一直在裸脚本里打转。等代码稳定了再补测试、补类型标注,完全来得及。

2. 动手前的准备:项目结构与打包方案选型

2.1 规范的目录结构长什么样

在Python打包界,目前主流推荐的是 src 布局。把真正的包代码放在一个src/目录下,能让打包工具和测试工具更准确地分辨“源码”和“构建产物”。这样做还有一个实际好处:在开发环境中,项目根目录下不会出现一堆临时文件,src/以外的位置即使有__init__.py,也不会被意外地当成包的一部分。一个典型的项目结构是这样的:

myproject/ ├── pyproject.toml # 包配置与构建声明 ├── README.md # 项目说明 ├── LICENSE # 开源许可证 ├── src/ │ └── mypackage/ │ ├── __init__.py # 包入口,声明版本号 │ ├── core.py # 核心功能模块 │ └── utils.py # 工具函数 └── tests/ └── test_core.py # 单元测试

这里有一个关键细节:包目录mypackage必须放在src/下面,而不是项目根目录下。很多人第一次打包失败,就是因为目录层级不对,导致import mypackage的时候找不到模块。文件夹的命名也要注意,包名应是小写字母、下划线或数字,不要用连字符-,因为import语句不支持连字符,my-package在 import 时会直接语法报错。如果你见过某些包叫my-package,那是它在 PyPI 上的显示名,真正 import 的名字一般是另一套,这点要分清楚。

2.2 打包工具怎么选:setuptools、hatchling、flit

现在Python推荐的构建后端主要有三套:setuptools、hatchling、flit。setuptools 是最老牌、兼容性最好的选择,生态里几乎所有的历史配置都能找到,社区问题解答也最多。如果你不太确定该用哪个,直接用 setuptools 基本不会错。hatchling 是后起之秀,配置更简洁,构建速度也快,新项目可以考虑。flit 则更适合个人小项目,它主打轻量,但有些高级功能支持得不够全。

三者的选择其实是在“兼容性”和“简洁性”之间做权衡。我个人的建议是:如果是公司内部使用,选 setuptools,遇到问题文档多、好排查;如果是个人新项目,想尝鲜,可以试试 hatchling,它的 pyproject.toml 写起来舒服很多。下面我用 setuptools 来演示,因为它的配置方式对绝大多数人来说最熟悉。这里也给你一个简单对照表,方便决策:

构建后端特点适合场景
setuptools生态成熟、兼容性好、资料多团队项目、需要稳妥方案的场景
hatchling配置简洁、构建快新项目、个人项目
flit极致轻量、上手快简单的小工具库

表格只是一个方向,真正选择时还要看你需要什么功能。比如你要声明 C 扩展或者自定义构建钩子,setuptools 的支持最稳;如果只是纯 Python 代码,三者的实际差异很小。所以我的最终建议是:第一优先级看团队熟悉度,第二看项目复杂度,别在工具上内耗太久,核心是产出结果。

2.3 pyproject.toml 核心字段逐项拆解

pyproject.toml 是这个体系里最核心的配置文件,pip 和构建工具都会读它。很多人一开始看到这个文件会有畏难情绪,觉得它又是一堆格式规范、构建声明,其实只要你理解了几个关键字段之间的关系,它就只是一张内容清单而已。配置文件负责回答三个问题:用什么工具构建、这个包叫什么、包里包含哪些内容。下面用一个最小可用的配置说明字段含义:

[build-system] requires = ["setuptools>=61.0"] build-backend = "setuptools.build_meta" [project] name = "mypackage" version = "0.1.0" description = "一个示例工具库" readme = "README.md" requires-python = ">=3.8" dependencies = ["requests>=2.20"] [project.optional-dependencies] dev = ["pytest>=7.0"] [tool.setuptools.packages.find] where = ["src"]

第一段[build-system]告诉 pip 用什么工具来构建这个包。requires列出构建时需要的基础工具,build-backend指定构建入口。这段配置一般照抄就行,但版本号值得留意:setuptools 低于61版的packages.find行为差异很大,所以建议写>=61.0。

[project]段声明的是包的元数据。name是包名,对应别人安装时用的名字;version是版本号,建议遵循语义化版本规则,比如0.1.0表示首个不完整版本,改动不兼容时变成1.0.0之类的规则要提前想好。dependencies是最重要的字段,它把运行所需的第三方库和版本约束都声明在这里,让 pip 安装时自动解析依赖,不用使用者手动一个个装。

最后一段[tool.setuptools.packages.find]是 setuptools 特有的配置,告诉构建工具去哪里找包。where = ["src"]表示到src/目录下扫描那些带__init__.py的文件夹。这一行配错或者漏掉,是最常见的“包装上了但 import 不了”的原因,因为 build 的时候它根本没把你的源码目录当成包的一部分。还有一个容易被忽略的点:如果你的包里有非.py的资源文件,比如配置文件、模板文件,那还要在[tool.setuptools.package-data]里单独声明,否则安装后文件不会跟着走。

3. 实操全过程:把一个脚本封装成可pip安装的库

3.1 第一步:把脚本改造成模块

假设你现在有一个脚本string_tools.py,里面有不少字符串处理函数。这个脚本很典型,日常写工具类代码时基本都会长成这个样子,函数之间互相调用,脚本顶部有一堆 import,底部可能还有几个手写的测试用例。现在我们要把它变成一个能被 pip 安装的包。先看一下核心内容:

def reverse_words(text: str) -> str: return " ".join(word[::-1] for word in text.split()) def count_vowels(text: str) -> int: return sum(1 for ch in text.lower() if ch in "aeiou")

第一步是把这个脚本变成包结构。在src/stringtools/下创建__init__.py,里面写:

from .core import reverse_words, count_vowels __version__ = "0.1.0" __all__ = ["reverse_words", "count_vowels"]

注意这里的导入用了相对导入from .core import ...,而不是from stringtools.core import ...。打包后的包名可能和内部模块名不完全一致,用相对导入可以避免踩到导入路径的坑。这是新手很容易忽略的细节:在包内部互相引用的时候,尽量不要写绝对导入,因为一旦包被改名或者被嵌套,绝对导入就可能出问题。同时建议把__version__定义在__init__.py里,这样用户可以通过stringtools.__version__查看版本号,排查问题时会非常方便。

另外,如果脚本里有if __name__ == "__main__"的入口代码,封装时想保留这个功能,可以把它拆到cli.py里,或者用后面提到的project.scripts来接。不要把入口逻辑写在包内部模块里,否则安装后其它人导入你的包时,这部分副作用代码也会跟着被触发,这通常不是你想要的。

这个小例子虽然简单,但封装的流程是通用的。实际项目中你的代码可能更复杂,但结构拆解思路完全一致:先定义清楚公开接口,再按模块整理实现。

3.2 第二步:写配置文件并构建安装包

把上面第2节的 pyproject.toml 内容保存到项目根目录,注意name和目录名保持一致,这里都用stringtools。然后执行构建:

pip install build python -m build

执行完你会看到项目根目录下生成了dist/文件夹,里面有两个文件:一个是.whl后缀的 wheel 包,另一个是.tar.gz后缀的源码包。wheel 是 pip 安装时用的预构建格式,安装速度快;源码包则是给人看的完整代码存档,也可以用来安装。如果你不想装build这个工具,也可以用旧一点的命令python setup.py sdist bdist_wheel,但官方现在已经不太推荐这种方式了,因为 setup.py 的执行逻辑不够透明,有时会悄悄执行你项目里的自定义代码。统一走python -m build会干净很多。

这里还有一个很多人会问的点:构建之前要不要把dist/里的旧文件删掉?我的习惯是每次构建前先清空dist/,避免残留旧版本的 wheel 干扰测试。写一行命令的事,省得后面分不清哪个是新的。使用build工具时,它是默认把当前目录当成项目根目录来读 pyproject.toml 的,所以最好在项目根目录下执行命令,别在子目录里喊,否则它找不到配置文件。

提示:如果你是在 macOS 或者 Linux 上构建,建议先确认当前 Python 版本和 pyproject.toml 里requires-python的约束一致,否则构建出的 wheel 可能带着一个很具体的版本标记,导致在其它环境装不上。

3.3 第三步:本地验证安装结果

构建出 wheel 之后,最稳妥的验证方式是在一个干净的虚拟环境里安装测试。因为如果你直接用当前的全局环境测试,很可能因为环境里已经装了某些依赖,导致没测出真正的问题。用虚拟环境把安装过程隔离起来,才能模拟一个普通用户从零安装的状态。

python -m venv venv # Windows: venv\Scripts\activate # Linux/macOS: source venv/bin/activate pip install dist/stringtools-0.1.0-py3-none-any.whl

装完以后进入交互式环境,验证两个点:第一,import stringtools能不能成功;第二,调用stringtools.reverse_words("hello world")能不能返回预期结果。这步通过,说明你的包在“标准安装”这条路上已经走通了。我记得第一次做这个步骤的时候,就卡在了pip install dist/...whl这一步,控制台提示找不到文件。后来才发现是 Windows PowerShell 的路径解析问题,直接改成 CD 到dist目录再执行就正常了。所以验证的时候,先确认你当前的 shell 路径确实指向了 wheel 文件所在的位置。

这里多说一句,本地验证不是可跳过的步骤。很多时候你写的配置在别人机器上会出问题,就是因为本地没严格按“标准安装”流程走过一遍。验证通过后,你的包才算是真正可交付的。

3.4 第四步:发布到PyPI(可选但推荐)

本地验证通过后,如果你希望别人也能通过pip install stringtools直接安装,就要把包发布到 PyPI。PyPI 是 Python 官方的包索引,类似手机上的应用商店。发布前需要先注册一个 PyPI 账号,然后在项目根目录创建~/.pypirc或者直接用命令行参数。最常用的发布工具是 twine:

pip install twine twine upload dist/*

上传时它会要求输入用户名和密码。注意:现在 PyPI 推荐用 API Token 代替密码,在账号设置里生成一个 Token,粘贴进去即可。上传成功后,等个一两分钟,你就能在自己的电脑上执行pip install stringtools安装体验了。上传之前建议先在 TestPyPI 上试一发,TestPyPI 是官方提供的测试环境,专门用来练手,可以避免不小心把坏包发到正式源上。

如果只是公司内部用,不想公开到 PyPI,可以把构建好的 wheel 文件放到内网文件服务器,或者部署一个私有的 PyPI 源(比如用 devpi 或者 Nexus 搭建)。安装时指定索引地址就行:

pip install stringtools --index-url http://你的内网源/simple/

私有源的好处是安全和可控,缺点是搭建和维护需要一点功夫。对于小团队,我其实更推荐先把 wheel 文件放在共享网盘或者代码仓库的 release 附件里,配合pip install <路径>就够用了,没必要一上来就搞一套源服务。等到团队规模变大、包的数量变多,再考虑私有源。

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

4.1 pip 安装报错的几种典型场景

我先说一个最近看到很多新手卡住的问题:控制台提示No module named pip,但明明是照着教程做的。这个一般不是你的环境坏了,而是你创建的虚拟环境里没有带 pip。低版本的虚拟环境可以用python -m ensurepip --upgrade来补装,或者直接删掉重建虚拟环境。项目里的坑:我之前用系统自带的 Python 3.7 创建虚拟环境时,遇到了 virtualenv 和 ensurepip 冲突的怪问题,最后老老实实换用官方 Python 3.11 才解决。环境问题有时候就是版本问题,不要在这上面死磕,重建环境往往才是最快的路径。

第二个典型问题是安装时提示Defaulting to user installation because normal site-packages is not writeable。意思是当前 Python 环境的包目录不可写,pip 退而求其次装到了用户目录。出现这个提示,最该做的是检查你是否在虚拟环境里。如果你确实在虚拟环境里,还提示这个,说明虚拟环境创建时没有正确关联,重新创建一次最省事。如果你就是想装到用户目录,那也能用,但要注意后续跨环境使用时包可能不在预期位置。

第三个是 pip 版本太旧导致构建失败。新建虚拟环境时,pip 版本可能偏老,而新版 setuptools 构建要求 pip 至少能解析 pyproject.toml 的构建依赖。命令很简单:

python -m pip install --upgrade pip

把这个当作配置新环境的固定动作,能避免很多莫名其妙的报错。别小看这一条,很多构建失败都不是代码的问题,纯粹是 pip 太老不认识新格式。

4.2 依赖、版本与环境的冲突处理

封装的库发布出去以后,最头疼的问题之一是依赖冲突。比如你的库依赖requests>=2.20,用户的另一个库却要求requests<2.20,这时候 pip 会报依赖无法满足。解决思路有几个,我按经验排序说一下。

第一个思路是尽量放宽依赖版本范围。不要写死requests==2.20.0,否则用户的其它库稍微动一下版本就装不了。用>=和<的组合把范围控制在“已验证过的区间”,比如requests>=2.20,<3.0。第二个思路是用 extras 机制区分强依赖和可选依赖。有些功能只有部分用户会用,对应依赖不该成为安装时的硬约束。在 pyproject.toml 里这样声明:

[project.optional-dependencies] extra = ["pandas>=1.0"]

用户需要这个功能时再执行pip install stringtools[extra]。第三个思路是要区分环境差异,特别是 Windows 和 Linux 下的二进制包。如果依赖里有 pandas、numpy 这种带 C 扩展的库,在 Windows 上最好装官方 wheel,别在 Linux 上构建的 wheel 拿过来硬装,往往会因为平台标记不匹配报错。查看一个 wheel 是否能装到当前环境,可以看文件名里的平台标签,比如win_amd64只能用在 Windows 64 位环境。

4.3 进阶玩法:私有源、离线安装与镜像加速

除了从 PyPI 官方源安装,实际工作中还经常遇到三种情况。

第一种是镜像加速。国内访问 PyPI 官方源经常很慢,手动安装时加-i参数指定镜像地址:

pip install stringtools -i https://pypi.tuna.tsinghua.edu.cn/simple

不想每次手敲,可以在用户目录下新建pip.conf(Windows 上是pip.ini),写入全局镜像配置,以后所有 pip 命令都自动走镜像。第二种是离线安装。生产环境经常没有外网,这时可以在一台有网的机器上预先下载好所有依赖的 wheel 文件:

pip download stringtools -d ./offline_packages -r requirements.txt

然后把offline_packages整个目录拷到目标机器,离线安装:

pip install --no-index --find-links=./offline_packages stringtools

离线安装时要特别注意依赖完整性,缺一个 wheel 文件就会失败。我实际操作时会在下载阶段就用pip download并把依赖一起下载,这样离线目录里通常已经包含了所有需要的包。

第三种是私有源。当你的库没打算公开上传到 PyPI,又想给团队提供统一的安装入口,可以自建一个简单的包索引。用 devpi 或者 Nexus 都行,结构也不复杂,本质上就是提供一个符合 PyPI simple API 的目录。搭建好后,团队成员把 index-url 配到私有源,安装时就会从私有源拉取内部包,公共依赖则会从上游代理拉取,体验和官方源几乎一致。这三种方式各有适用场景,镜像加速适合个人开发,离线安装适合生产部署,私有源适合团队协作。

5. 封装后还能做什么:进阶扩展方向

5.1 给你的库加上命令行入口

如果你封装的库不只是提供函数,还希望使用者可以在终端里直接运行某个命令,可以在 pyproject.toml 里声明命令行入口。比如你的包里有这样一个主函数:

def main(): print("hello from stringtools") if __name__ == "__main__": main()

然后在 pyproject.toml 里加上:

[project.scripts] stringtools-cli = "stringtools:main"

安装后,用户在终端输入stringtools-cli,就会执行stringtools.main()。这个机制在发布工具类、CLI 类库时非常有用。接入的入口不需要是包内的模块函数,只要是从包名能解析到的对象就行,所以简单工具在__init__.py里直接定义 main 也可以。

有一点要注意:命令名和包名可以不同,但为了避免和系统里已有命令冲突,建议命令名带一点项目标识。比如包叫stringtools,命令可以叫st或stringtools,但别起clean、setup这种过于通用的名字。还要记得在命令行入口里处理sys.argv参数,或者直接用 argparse、click 这类库来写参数解析。我这里常用的做法是用click,它的装饰器风格配合标准包结构,写出来的命令工具可读性很高。

5.2 类型标注、文档与测试的配套

封装只是第一步,一个真正“好用”的库还需要三件套:类型标注、文档和测试。

类型标注是现代 Python 库的标配。在函数签名里加上类型提示,配合 mypy 或 pyright 可以直接在开发阶段发现很多类型错误,也能让 IDE 的自动补全好用很多。发布时如果不想引入额外的运行时依赖,可以用py.typed标记文件来声明你的库有类型信息,这对使用者非常友好。我现在的做法是每个公开函数都写类型标注,至少写完__init__.py里导出的那些函数,排查问题时会感觉非常顺畅。

文档方面,README 至少要写清楚:这个库解决什么问题、怎么安装、怎么用。复杂一点的库可以生成 API 文档,比较常用的方案是 Sphinx 或 MkDocs,配合 docstring 自动抽取函数说明。不过我建议先别上来就搞全家桶,把 README 写好,给出三五个能跑通的最小示例,就能解决大部分用户的需求。

测试就不用多说了,尤其当你发布给团队或开源社区使用时,测试就是你的安全保障。pytest 是现在的主流选择,在tests/目录下写用例:

from stringtools import reverse_words def test_reverse_words(): assert reverse_words("hello world") == "olleh dlrow"

每次改代码前跑一遍测试,能帮你挡住很多低级回归错误。测试文件通常不会被装进安装包,只留在源码仓库里,这也没有问题,因为使用者只需要成品,不需要测试源码。

5.3 版本管理与持续集成

当你的库从“自用工具”升级为“团队基础设施”,版本管理和持续集成就提上日程了。版本管理上,除了语义化版本,我建议用 git tag 来标记发布的每个版本,比如v0.1.0。这样每个版本对应一个可回溯的代码状态。发布流程可以固定成:改版本号 -> 构建 -> 测试 -> 打 tag -> 上传 PyPI,每一步都检查无误再继续。

持续集成方面,GitHub Actions 是开源项目里最常用的。一个基础的流水线可以做到:push 时自动跑测试;打 tag 时自动构建并发布到 PyPI。只要在.github/workflows/release.yml里写一个简单的 yaml,就能省掉手动构建上传的重复劳动。这里我不展开完整配置,但提醒一句:发布到 PyPI 的 Token 一定要存成 CI 的 secret,不要明文写在仓库里,否则等于把仓库的包发布权限暴露给了所有能读到仓库的人。

版本发布还有一个实用技巧:把版本号从 pyproject.toml 里抽出来,用代码生成的方式来管理。比如 setuptools-scm 可以根据 git tag 自动生成版本号,这样你永远不用担心手动改版本号和 git tag 对不上。配置好之后,一次python -m build就能拿到带正确版本号的安装包。

做了这么多封装相关的事,我个人最深的体会是:封装本身不难,难的是想清楚边界。一个库该暴露什么接口、依赖范围怎么圈定、版本策略怎么定,这些设计决策往往比打包命令重要得多。好的库不是功能越多越好,而是用起来越省心越好。到了这一步,你的代码就不再只是“能用”,而是真正“可交付”了。

分享一个小技巧:在自己电脑上做封装测试时,多用pip install -e .安装开发模式。这种模式下,包里的代码修改后不用重新安装就能看到效果,调试效率提升不是一点半点。等你确认一切正常了,再用正式构建的方式生成 wheel 去验证。我自己在开发内部库的时候,几乎全程都在用-e模式,踩坑率低很多。

这个封装的思路还能继续扩展:比如把你的命令行工具做成独立入口,把动态版本号接进 CI 流程,或者把内部包推到私有源给整个团队使用。我自己是越用越觉得,这套流程一旦跑通,后面每一次新库的发布都能省下大把时间。从最小可用的包开始动手,先跑通一个完整的 pip install 链路,剩下的事都会顺理成章。

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

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

立即咨询