
我见过太多Python项目在“本地没问题一到服务器就各种诡异”的状态里反复折腾有的同学把项目代码用scp传到服务器然后手工pip install装完一跑发现少了个系统库有的同学用virtualenv把环境打包带过去结果换一台机器就崩。这些问题的根子都一样——你没法保证开发、测试、生产三套环境完全一致。今天这篇就来聊Python部署里最实用的进阶手段用Docker把Python项目做成镜像让构建、分发、运行三个环节彻底可复现。文章会从Dockerfile怎么写、镜像分层机制怎么利用、多阶段构建怎么瘦身一直讲到真实部署中容易踩的细节全程基于我在日常项目中反复验证过的做法可以直接照抄。先交代一下背景方便大家判断这篇文章适不适合你假设你已经能用Python写项目、会用pip管理依赖但对Docker的理解停留在“听过、看过、没认真用过”的状态。那么接下来这几千字会帮你把“把Python项目打包成镜像”这件事彻底想明白而不是停留在“能跑就行”的黑盒操作。1. 容器镜像不是“套了个壳”那么简单先搞懂构建链路上的三个关键角色很多人第一次接触Docker看到docker build之后凭空冒出一个镜像就以为镜像就是“把我的代码压缩成一个包”。这个理解不算全错但对于真正想把镜像构建玩明白的人来说不够。你要跟镜像构建打交道就必须搞懂三个角色Dockerfile、构建上下文、镜像层。1.1 Dockerfile是你的“构建说明书”Dockerfile的本质是一份按行执行的构建脚本。每一行指令都会生成一个只读的镜像层层与层之间是叠加关系。举个例子下面这个最简DockerfileFROM python:3.11-slim WORKDIR /app COPY requirements.txt . RUN pip install -r requirements.txt COPY . . CMD [python, app.py]从FROM开始Docker会拉取一个基础镜像然后在它的基础上依次执行WORKDIR、COPY、RUN、CMD。每一行都对应一个层最终这些层叠加起来就是你看到的镜像。这个机制最大的好处是层可以复用——如果你只改了最后几行前面的层在本地有缓存的话就不需要重新执行。但代价是如果你把COPY . .写在前面把RUN pip install写在后面那么只要项目里任何一个文件变动整个依赖安装层就会全部失效构建时间直线上升。1.2 构建上下文别把整个项目都喂给Docker构建上下文是另一个容易被忽略的概念。docker build后面的那个.指的是把当前目录整个打包发送给Docker守护进程Dockerfile里的COPY只能从这份“上下文”里取文件。很多初学者会不小心把node_modules、.git、本地__pycache__、虚拟环境目录统统打进去导致上下文体积巨大构建慢镜像里还混入一堆垃圾文件。正确做法是在项目根目录放一个.dockerignore像写.gitignore一样把不需要的文件排除掉.git __pycache__ *.pyc .venv venv .env .pytest_cache .mypy_cache dist build *.log.dockerignore和镜像体积的关系很多人低估了。上下文越小docker build的执行速度越快尤其当你把镜像推到远程仓库时影响会更直观。1.3 镜像层、容器层、可写层运行时的故事最后稍微聊一下运行时。镜像本身是只读的docker run的时候Docker会在镜像最上面加一个可写层所有运行时的文件修改、日志写入、临时文件都发生在这个可写层。这意味着两个事情第一容器删除后可写层里的一切都会消失第二如果你在镜像里用tail -f去读日志文件写进可写层的日志在你docker rm之后也没了。所以日志、数据、临时文件这些东西必须挂载到宿主机或外部存储而不是依赖容器内部的文件系统。这个道理后面讲部署细节时还会用到。说到底搞懂这三个角色你才真正理解了“镜像构建”是发生在构建期的静态过程而“容器运行”是构建之后的动态过程。很多人部署出问题就是把这两件事混在一起看。2. 从零写一份能直接上生产的Python Dockerfile选型、顺序与启动命令准备好常识之后下面进入正题写一份用于生产环境的Python镜像Dockerfile。这部分我会一步步拆解每一个决策背后都尽量说清楚理由而不是抛一个“标准答案”让你背。2.1 基础镜像选型slim、alpine还是全量版基础镜像的选择直接影响镜像体积、安全性和兼容性。常见的Python基础镜像有几种镜像标签体积特点适用场景python:3.11较大基于完整系统包含大量编译工具和系统库本机调试、需要复杂编译的场景python:3.11-slim中等基于精简系统去掉了编译工具链和很多系统包大多数常规Python项目首选python:3.11-alpine最小基于Alpine精简系统体积极小但部分C扩展需要额外编译对体积极度敏感、依赖纯Python或自带轮子的项目我的建议是不要一上来就上alpine。Alpine的问题在于它使用musl libc和大多数Linux发行版使用的glibc存在差异有些带C扩展的Python包比如pydantic-core、cryptography、psycopg2虽然在近期版本里已经有了很多兼容支持但偶尔还是会遇到“本地编译好alpine里跑不了”的坑。对于大多数业务项目slim版本是最平衡的选择体积不算大系统包够用pip安装大部分预编译轮子都顺畅。举例来说一个用FastAPI写的项目依赖里有uvicorn、sqlalchemy、pydantic如果用python:3.11-slim作为基础镜像整个镜像构建下来通常能控制在200MB以内如果用alpine能压到100MB左右但一旦遇到需要gcc现场编译的依赖构建时间会暴涨反而得不偿失。2.2 依赖安装的黄金顺序把requirements.txt的缓存价值发挥到极致Dockerfile里指令顺序的设计核心目标只有一个——最大化利用构建缓存。几乎所有Python镜像构建的性能问题都出在把COPY和RUN pip install的顺序排错了。下面这段顺序是我在多个项目里验证过的黄金排列FROM python:3.11-slim # 1. 先设置环境变量避免Python在容器里出现字节码缓存和输出缓冲问题 ENV PYTHONDONTWRITEBYTECODE1 \ PYTHONUNBUFFERED1 # 2. 设置工作目录 WORKDIR /app # 3. 先只复制依赖清单文件不要复制全部代码 COPY requirements.txt . # 4. 安装依赖 RUN pip install --no-cache-dir -r requirements.txt # 5. 最后才复制全部源码 COPY . .这样写的好处很直观只要requirements.txt没变第4步的依赖安装层就一直命中缓存。你改一行业务代码rebuild只需要重新执行最后两步如果你把COPY . .放在pip install前面那每次改代码都会触发整个依赖层重建轻则多等几分钟重则每次都要重新下载几十个包。这里还要吐槽一个常见问题很多教程让你直接RUN pip install -r requirements.txt完全不考虑--no-cache-dir。pip默认会缓存下载的轮子在普通机器上这是好事但在镜像层里这些缓存会永久留在文件系统里白白增加几十MB甚至上百MB体积。所以生产环境构建镜像--no-cache-dir应该成为肌肉记忆RUN pip install --no-cache-dir --upgrade pip \ pip install --no-cache-dir -r requirements.txt2.3 进程启动命令CMD和ENTRYPOINT怎么选信号处理怎么做接着是启动命令。Python项目的启动方式五花八门python app.py、gunicorn -c gunicorn.conf.py app:app、uvicorn main:app --host 0.0.0.0 --port 8000。在Dockerfile里你的执行入口有两种声明方式。CMD是默认启动命令会被docker run后面跟的命令覆盖。ENTRYPOINT则定义容器启动时一定会执行的程序且不容易被覆盖。常见的组合是把ENTRYPOINT写成固定入口把CMD写成默认参数ENTRYPOINT [gunicorn] CMD [-c, gunicorn.conf.py, app:app]但这里有个容易踩的暗坑容器里的进程信号处理。如果你用python app.py直接启动Python进程就是PID 1直接接收来自docker stop的SIGTERM信号正常走完try/finally之类的清理逻辑后退出体验最平滑。如果你用sh -c python app.py或者直接写一个start.sh那么PID 1是shellshell能不能把信号转发给子进程完全取决于你的脚本怎么写。很多容器用docker stop之后迟迟不退出就是信号没转发到位。在实际项目中我推荐用exec方式的启动脚本保证信号直达COPY start.sh . RUN chmod x start.sh ENTRYPOINT [./start.sh]start.sh里用exec启动真正的进程#!/bin/bash exec gunicorn -c gunicorn.conf.py app:appexec会把shell进程替换成gunicorn这样PID 1就是gunicorn本身信号能正确处理。同理如果你用uvicorn也建议用上面的方式。3. 真正决定镜像构建速度和体积的分层缓存机制写Dockerfile这件事表面看是“照着模板抄”实际上真正拉开水平差距的是你对分层缓存机制的理解深度。这一节我结合一个具体案例来展开把“为什么指令顺序会影响构建时间”这件事彻底讲透。3.1 镜像层的复用逻辑改一行代码到底有多少层会重建镜像层缓存的粒度是“指令”。每一条指令执行前Docker会先检查请求的指令和上下文是否和上一次构建完全一致。如果一致就直接复用上次生成的层只要有一个条件不满足比如Dockerfile某一行文本变了、被COPY的文件内容变了那么从这一行开始的后续所有层全部重建。我见过一个真实的例子某项目把Dockerfile写成这样FROM python:3.11-slim WORKDIR /app COPY . . RUN pip install --no-cache-dir -r requirements.txt CMD [python, app.py]项目里有几十个Python文件开发期间几乎每天都有改动。结果就是每次构建COPY . .都因为文件变化而失效紧接着的pip install也被迫全部重来。依赖里碰巧又有numpy、pandas这类重包一次构建要跑5分钟以上。后来把依赖安装挪到代码复制前面改业务代码后的重启构建时间从5分钟降到10秒左右——只挪了三行代码的位置效果天差地别。3.2 提高缓存命中率的三个文件组织习惯想要把缓存命中率拉满光调对顺序还不够。我在实际项目中还会做这些事第一把各种依赖配置文件提前复制。除了requirements.txt如果你用了poetry就把pyproject.toml和poetry.lock一起提前复制再执行poetry install。让Docker知道“这些锁文件没变依赖层就可以不重建”。第二把频繁变动和极少变动的文件分开目录。比如你的项目里有一个由脚本自动生成的proto文件目录它经常变化而主体业务代码不太变那么你可以在Dockerfile里单独COPY这个目录让它的更新不影响其他依赖层。虽然大多数项目没这么复杂但这个思路能帮你少续好多次构建。第三锁住依赖版本。这句话既是对Docker说的也是对人说的。requirements.txt里如果写了numpy1.24而不是numpy1.24.3那么每次你重新构建镜像pip拉到的版本都可能悄悄变化缓存命中完全是运气。更重要的是你无法保证生产环境装到的版本就是你本地测试的版本。把版本号锁死让镜像构建和运行都可复现是上生产的第一课。3.3 实测数据调整顺序后构建时间对比为了让大家对这个优化有直观感受我这里复盘一个模拟项目X的实测数据。项目结构大概是FastAPI SQLAlchemy psycopg2-binary pydantic代码文件约200个总大小约30MB。未优化的Dockerfile即先COPY . .再RUN pip install首次构建耗时约3分40秒。在不改requirements.txt的情况下修改一个业务代码文件后再次构建耗时约3分20秒——因为依赖层全废了。同样的项目把requirements.txt提前复制依赖安装后统一COPY . .首次构建耗时约3分30秒差别不大因为首次都得完整跑但改一个业务代码文件后再次构建耗时约8秒。八秒和三分半这就是指令顺序带来的差距。4. 多阶段构建让最终镜像只剩运行时真正需要的东西很多Python项目里开发依赖和运行依赖混在一起。比如项目用了black做格式化、pytest做测试、mypy做类型检查这些工具在开发阶段很好用但它们完全没有必要进入生产镜像。多阶段构建multi-stage build就是解决这个问题的标准姿势。4.1 为什么需要多阶段构建单阶段构建的问题是镜像会把构建过程中所有安装过的东西都保留下来。你要用gcc源码编译某个包gcc整个编译工具链也会跟着进镜像你要跑测试pytest和它的所有依赖也被带进镜像。最终体积不断膨胀而运行容器根本不需要这些。多阶段构建的思路是在第一个阶段构建阶段完成依赖安装、代码编译、测试运行等所有“重活”在第二个阶段为主。字面上就是这么简单——先写一个包含全量工具的“建造者”镜像把项目准备好然后换一个干净的“运行者”镜像只把真正需要的东西拷过去。4.2 一个带C扩展项目的最标准写法拿一个实际场景举例假设我的项目依赖里有一个需要编译的包比如旧版的lxml或者pydantic或者项目里有需要cythonize的代码那么完整的Dockerfile可以是这样的# 第一阶段构建阶段装包含编译工具的所有依赖 FROM python:3.11-slim AS builder # 安装构建所需的基础系统包 RUN apt-get update apt-get install -y --no-install-recommends \ build-essential \ libpq-dev \ rm -rf /var/lib/apt/lists/* WORKDIR /build COPY requirements.txt . RUN pip install --no-cache-dir --prefix/install -r requirements.txt COPY . . # 可以在这一阶段跑测试 RUN pytest tests/ --ignore/build/venv # 第二阶段运行阶段尽量精简系统包 FROM python:3.11-slim # 只装运行期真正需要的系统动态库不装编译器 RUN apt-get update apt-get install -y --no-install-recommends \ libpq5 \ rm -rf /var/lib/apt/lists/* WORKDIR /app # 从builder阶段把安装好的依赖文件整体拷过来 COPY --frombuilder /install /usr/local # 再把业务代码拷过来 COPY . . # 创建非root用户运行容器安全考虑 RUN useradd -m appuser USER appuser EXPOSE 8000 CMD [gunicorn, -c, gunicorn.conf.py, app:app]这里有几个细节值得展开。第一--prefix/install指定了pip安装的位置这样第二阶段用COPY --frombuilder /install /usr/local把依赖整体搬过去Python就能直接import到。第二libpq-dev是编译psycopg2时需要的头文件而运行时只需要libpq5这个动态库所以在第二阶段只装了后者——编译器进了第一阶段镜像最终生产镜像里完全没有gcc。第三apt-get装完包之后立刻rm -rf /var/lib/apt/lists/*清理索引缓存这点和pip --no-cache-dir同样重要能省掉几十MB上下文里看不到的空间。4.3 非root用户运行容器安全与兼容的平衡上产之前一定要做的另一件事是容器内不要用root用户跑业务进程。原因很直白容器和宿主机共享内核如果业务进程以root身份运行一旦容器被攻破攻击者获得的就是宿主机上的root权限这在任何部署环境里都是不可接受的风险。做法很简单在Dockerfile末尾加上用户创建与切换RUN useradd --create-home --shell /bin/bash appuser USER appuser但注意USER指令不是只有这一句它涉及权限的连锁反应。比如你COPY进去的文件默认owner是root换成appuser之后可能没有读取权限你的应用如果需要在容器内写文件比如缓存目录还要提前处理好目录权限RUN mkdir -p /app/data chown -R appuser:appuser /app这一点在本地开发时不太容易暴露因为本地跑容器经常用--user $(id -u)强行指定但在生产环境或者当别人直接docker run你的镜像时非root用户和文件权限问题就会冒出来。5. 部署到实际环境时最容易出问题的四个细节镜像构建成功了不代表部署就顺了。接下来这部分我把自己在真实环境里踩过的坑按主题整理一遍每一条都是网上教程很少提到、但生产环境几乎一定会上演的戏码。5.1 时区、编码与locale问题第一个容易翻车的点是时区与编码。Docker基础镜像默认时区是UTC很多基础镜像甚至没有装locales。如果你的业务里涉及时间格式化、日期计算或者日志里需要显示本地时间就会遇到两种诡异情况时间对不上、或者某些中文字符串在日志里变成乱码。解决方案是在Dockerfile里显式配置ENV TZAsia/Shanghai RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime echo $TZ /etc/timezone如果你的镜像里没有/usr/share/zoneinfo目录先通过apt-get install -y tzdata补上。至于中文字符乱码多半是系统缺少中文字体或locale导致的在slim镜像上建议加上ENV LANGC.UTF-8 \ LC_ALLC.UTF-8C.UTF-8是Linux系统里内置的基本locale多数情况下不用额外安装就能保证UTF-8编码正常。这个小配置我以前吃过亏项目在本地跑得好好的部署到容器里一打印中文日志就是UnicodeEncodeError查了半天才发现根本没有设定locale。5.2 容器访问数据库和外部服务的网络模型容器里的localhost指的是容器自己不是宿主机。这个常识很多人都知道但真的到连数据库的时候还是不断有人踩坑在容器里用localhost:5432连宿主机上的PostgreSQL反复重试连接失败又找不出原因。正确的理解是容器和宿主机之间的网络访问有几种模式桥接模式默认容器内进程访问宿主机服务使用host.docker.internalDocker桌面端支持或者宿主机在局域网中的IP容器之间互相访问通过--link或自定义网络。主机模式--network host容器直接共享宿主机的网络栈这时候localhost就是宿主机。大多数生产环境里数据库和应用本身都是跑在同一个Docker网络里的独立容器所以写连接配置时数据库地址要写成服务名而不是localhost。比如用docker-compose部署时你的服务名是db应用容器里就应该配置DB_HOSTdb。这件事和Dockerfile本身无关但它是“镜像构建之后一定会遇到”的部署问题。如果你把数据库地址写死在了代码里而没做成环境变量那镜像就少了一层灵活性——同样的镜像换个环境就玩不转。这也是我在所有项目里都坚持把连接字符串、密钥、开关类配置放到环境变量的原因。5.3 健康检查与优雅停机让业务无感重启容器编排工具比如某容器管理平台或docker-compose的restart策略都会对容器做健康检查。如果我们的镜像没有提供健康检查机制平台就只能靠“进程是否存活”来判断这其实是非常粗糙的。我推荐在Dockerfile里加HEALTHCHECK指令。假设项目是FastAPI可以这样写HEALTHCHECK --interval30s --timeout3s --start-period5s --retries3 \ CMD python -c import urllib.request; urllib.request.urlopen(http://localhost:8000/health, timeout2) || exit 1这样编排平台每隔30秒就会探测一次/health接口连续失败3次就判定容器不健康。有了这层机制发布新版本时才能实现流量切换和旧容器回收之间的安全交接没有它容器挂了可能要到外部用户报障你才反应过来。顺带提一下优雅停机。前面说了用exec启动进程这是“能收到退出信号”的起点但还要保证你的Web框架真的能处理退出信号。像gunicorn和uvicorn都自带优雅退出的处理收到SIGTERM后会停止接收新请求、等待已有请求处理完再退出你需要做的只是留出足够的等待时间不要让编排平台在几秒内直接SIGKILL。配置stop_grace_period的时候我一般会留10到30秒。5.4 日志直接打到stdout不要写文件容器里的文件系统是临时的最好假设容器随时可能被删掉重建。把日志写进文件意味着容器删除后日志全部消失排查问题只能靠宿主机文件系统的残留或者更惨——什么都没有。行业的通行做法是让应用直接把日志输出到stdout/stderr由容器运行时负责收集。Python里如果用logging默认配置就会输出到stderr如果用gunicorn默认也是输出到控制台。整合起来的建议是不要在Python代码里配置FileHandler一切日志走标准输出除非你有非常特殊的合规要求需要额外做一份落盘日志。这一点在Kubernetes这类环境里几乎是强制的容器运行时会把stdout/stderr统一收集成日志文件并配合日志采集组件做聚合。如果你写的日志散落在容器内部文件系统里采集系统根本看不见。6. 一次真实的镜像瘦身过程从848MB到118MB光讲理论容易飘最后用一个模拟项目X的真实案例复盘镜像优化的完整路径。某内部项目是一个基于Python的异步数据采集服务依赖里有aiohttp、pandas、pymongo、cryptography等代码通过单阶段Dockerfile构建后镜像体积达到了848MB。这个体积在日常推送和拉取时非常痛苦尤其是网络不太给力的时候镜像分发基本是一场灾难。6.1 原始Dockerfile的问题拆解原始Dockerfile大致长这样FROM python:3.11 WORKDIR /app COPY . . RUN pip install -r requirements.txt CMD [python, main.py]问题一眼就能看到好几处第一基础镜像用了全量python:3.11自带大量用不到的编译工具和文档体积一下就大了两三百MB第二COPY . .在前导致每次代码变动依赖全重建构建速度也受到牵连第三没有清理apt和pip的缓存镜像里堆了一堆临时文件第四没有做多阶段构建编译构建产物全留在最终镜像里。6.2 每一步优化动作和效果对比我按顺序做了这些优化每一步都手动记录镜像体积步骤操作镜像体积原始状态基础镜像python:3.11单阶段构建848MB第一步基础镜像换成python:3.11-slim512MB第二步调整指令顺序先复制依赖清单再安装后复制代码给pip加--no-cache-dir486MB第三步加入.dockerignore排除.venv、.git、__pycache__、测试文件472MB第四步用多阶段构建将编译型依赖隔离在builder阶段运行镜像只保留二进制产物和动态库198MB第五步运行阶段改用python:3.11-slim而不是builder阶段的完整镜像apt包最小化只装运行时动态库145MB第六步最终全面清理apt缓存、pip缓存并通过docker history排查每个层的体积删除无用层118MB从848MB到118MB压缩掉86%。最夸张的是第四步和第五步叠加效果——因为项目里有pandas和cryptography这两个包在构建时会拉起大量编译工具链这些全被多阶段构建挡在了运行镜像之外。6.3 最终Dockerfile结构复盘优化后的核心结构如下FROM python:3.11-slim AS builder RUN apt-get update apt-get install -y --no-install-recommends \ build-essential \ rm -rf /var/lib/apt/lists/* COPY requirements.txt . RUN pip install --no-cache-dir --prefix/install -r requirements.txt FROM python:3.11-slim COPY --frombuilder /install /usr/local WORKDIR /app COPY . . RUN useradd -r appuser USER appuser ENV PYTHONDONTWRITEBYTECODE1 \ PYTHONUNBUFFERED1 \ TZAsia/Shanghai CMD [python, main.py]这份Dockerfile既兼顾了体积也兼顾了安全非root用户、可维护性依赖分层缓存和运行时表现。如果你现在要为一个常规Python项目写Dockerfile照着这个思路走基本不会错。6.4 镜像瘦身后的实测表现瘦身完成后我专门观察了一段时间。镜像推送时间从原来的接近2分钟缩短到20秒左右测试环境扩容时的冷启动速度也有明显提升。更意外的是安全性也顺带变好了——最终镜像里连gcc都没有攻击者在容器里就算拿到shell也没有编译器可用想现场编译个提权工具都难。这是体积优化带来的额外福利非常划算。7. 一些构建镜像时容易忽略的日常经验除了上面那些比较系统性的内容还有几条零散经验是在实际写Dockerfile和调部署时积累下来的。每一条都短但都有人因此卡过很久。第一调试构建问题时别只用docker build的默认输出。建议加上--progressplain参数再构建一次能看到每一步的完整输出和耗时定位慢的层非常有效。如果觉得还是不够直观可以用docker history image查看每个层的体积变化——镜像体积异常膨胀的时候这招能帮你快速锁定是哪个指令把东西装进来了是pip install的缓存还是某个COPY把大文件捎带了。第二安装依赖时用--no-cache-dir装系统包后用rm -rf /var/lib/apt/lists/*。这两步几乎是Python镜像瘦身的基础动作成本为零收益稳定。如果你用apt-get install装完包后不做清理每一个镜像里都会多出几十MB已经没用的索引文件。第三Dockerfile里同一个指令的多次执行之间尽量合并。比如多个RUN apt-get install要合并在一条RUN里执行多个ENV也可以合并。指令越少层数越少镜像越扁平构建和推送都会更快。第四镜像构建阶段不需要跑测试的话就别把测试依赖装进依赖清单。生产镜像里如果带着pytest、coverage、black这些开发工具不仅体积膨胀也给安全审计增加风险面。测试放在CI阶段做做完再构建生产镜像这才是正常流程。最后说一句关于docker-compose和构建参数生产环境下构建镜像时经常需要把不同的配置传进Dockerfile。比如你要在构建时指定某个参数版本号、环境标识用ARG而不是ENV。ARG是构建期的变量ENV是运行期的环境变量。这个区别看着不起眼但用错的人非常多。用ARG传入的构建参数在镜像构建完就不会留在运行环境里这对安全很重要——有些密钥不该进入运行环境的话就只该用ARG在构建期使用或者干脆用--build-arg配合单独的安全存储机制而不是直接写死在Dockerfile里。说到底Python项目的Docker容器化写Dockerfile只占了30%的工作量剩下的70%是对镜像分层机制的理解、对部署细节的把握和持续迭代优化。这篇我把自己在真实项目里踩过的坑和验证过的方法基本都列出来了。如果你正打算把一个Python项目容器化先照这份思路写出一版比“能跑就行”更讲究的Dockerfile再在实际部署中慢慢打磨细节我相信你会反过来感谢那个把COPY . .放在pip install前面然后每次构建都等三分钟的自己。