解决Python使用gRPC编译proto后运行出现ModuleNotFoundError: No module named ‘XXX_pb2’
报错内容
File "/cyclegen_grpc_project/proto/custom_pb2_grpc.py", line 4, in <module>
import custom_pb2 as custom__pb2
ModuleNotFoundError: No module named 'custom_pb2'
背景介绍
在Python使用中使用gRPC工具生成pb文件时,我们通过以下命令来编译proto
文件:
python -m grpc_tools.protoc -I proto --python_out=. --grpc_python_out=. proto/*.proto
经过编译后每个proto
会生成*_pb2.py
和*_pb2_grpc.py
,我们在使用的时候需要继承*_pb2_grpc.py
中的XXXServicer
类来实现自己的Python服务端类,使用XXXStub
来实例化客户端发起调用。
错误复现
假设我们的proto
存放目录如下:
cyclegen_grpc_project/
proto/
custom.proto
然后我们在proto
目录下使用:
python -m grpc_tools.protoc --proto_path=. --python_out=. --grpc_python_out=. custom.proto
进行编译,会得到如下的生成结果:
cyclegen_grpc_project/
proto/
custom.proto
custom_pb2.py
custom_pb2_grpc.py
然后我们调用custom_pb2_grpc.py
的客户端或服务端,报错:
File "/cyclegen_grpc_project/proto/custom_pb2_grpc.py", line 4, in <module>
import custom_pb2 as custom__pb2
ModuleNotFoundError: No module named 'custom_pb2'
然后我们在IDE中打开custom_pb2_grpc.py
会发现在文件第4行左右中import custom_pb2 as custom__pb2
被标红了,custom_pb2
无法被Import
,gRPC试图使用绝对路径来导入custom_pb2
报错分析
由于*_pb2_grpc.py
会使用*_pb2.py
中定义的各种类型,因此两者之间存在import
关系,而问题恰恰出在这个import
上。
注意我使用的指令:
*python -m
grpc_tools.protoc
--proto_path=.
--python_out=.
--grpc_python_out=.
custom.proto
中间的空格都被我给区分开了,上述指令的意思我们解读一下:
custom.proto
将所在目录为当前目录的`custom.proto
进行处理
--proto_path=.
将当前目录添加到搜索路径,用这个参数来寻找你proto
文件中的import
的文件路径
--python_out=.
生成的_pb2.py
存放于当前目录
--grpc_python_out=.
生成的_pb2_grpc.py
存放于当前目录
错误原因
我在Github上找到了issue,问题在custom.proto
路径,同时也是执行目录的问题(可能不太好理解)。
Python下protoc的解析方案是根据proto
文件的相对路径来确定导入路径的,也就是说,假如我的目录如下:
cyclegen_grpc_project/ -------> 在该层运行指令
proto/
custom.proto
我如果想正确的编译,那么我应该在cyclegen_grpc_project
下使用
python -m grpc_tools.protoc --proto_path=. --python_out=. --grpc_python_out=. proto/custom.proto
注意到指令末尾改为了相对路径proto/custom.proto
,而--python_out
和--grpc_python_out
的目录相对于当前目录,执行完该指令后,protoc将会在--python_out
生成proto/custom_pb2.py
,在--grpc_python_out
下生成proto/custom_pb2_grpc.py
,一般我们会将--python_out
和--grpc_python_out
指定为同一个目录。
再次展示我们的项目文件路径:
cyclegen_grpc_project/ -------> 在该层运行指令
proto/
custom.proto
custom_pb2.py
custom_pb2_grpc.py
此时可以打开custom_pb2_grpc.py
发现其引用变为了from proto import custom_pb2 as proto_dot_custom__pb2
可以正常被导入。
解决方案
- 修改
custom_pb2_grpc.py
将import custom_pb2 as custom__pb2
修改为使用相对路径来进行导入from . import custom_pb2 as custom__pb2
- 从项目顶层使用指令,将待编译的
proto
文件相对路径填写完整,而--python_out
和--grpc_python_out
直接使用项目顶层路径,即当前位置,该方法实际上相当于会创建proto
文件相对路径中所有的文件夹,即直接保存在proto
所在目录,例如:cyclegen_grpc_project/ -------> 在该层运行指令 proto/ custom.proto
运行
python -m grpc_tools.protoc --proto_path=. --python_out=. --grpc_python_out=. target_dir/custom.proto
编译后的目录结构:
cyclegen_grpc_project/ -------> 在该层运行指令 proto/ custom.proto custom_pb2.py custom_pb2_grpc.py
- 如果不想将
proto
文件和编译产生的_pb2.py
和_pb2_grpc.py
放在一起,可以在一个临时目录下创建你要储存的路径,据个例子:cyclegen_grpc_project/ -------> 在该层运行指令 tmp/ target_dir/ custom.proto
运行
python -m grpc_tools.protoc --proto_path=tmp --python_out=. --grpc_python_out=. target_dir/custom.proto
这里使用
--proto_path=tmp
来保证protoc
会在tmp
下搜索target_dir/custom.proto
,然后--python_out
和--grpc_python_out
直接使用项目顶层路径,即当前位置,则会使得protoc
将target_dir
创建在顶级目录下,编译后目录结构:cyclegen_grpc_project/ -------> 在该层运行指令 tmp/ target_dir/ custom.proto target_dir/ custom_pb2.py custom_pb2_grpc.py
这样就成功修改了编译后文件的路径了。
简单来说,通过假定
proto_path
为项目目录,在该目录下创建你要储存的文件路径镜像来实现修改目录的功能。
本文系作者 @root 原创发布在 CycleGen。未经许可,禁止转载。