解决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

可以正常被导入。

解决方案

  1. 修改custom_pb2_grpc.pyimport custom_pb2 as custom__pb2修改为使用相对路径来进行导入from . import custom_pb2 as custom__pb2
  1. 从项目顶层使用指令,将待编译的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
    

     

  2. 如果不想将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直接使用项目顶层路径,即当前位置,则会使得protoctarget_dir创建在顶级目录下,编译后目录结构:

    cyclegen_grpc_project/                 	-------> 在该层运行指令
    	tmp/
    		target_dir/
    			custom.proto
        target_dir/
    			custom_pb2.py
    			custom_pb2_grpc.py
    

    这样就成功修改了编译后文件的路径了。

    简单来说,通过假定proto_path为项目目录,在该目录下创建你要储存的文件路径镜像来实现修改目录的功能。

本文系作者 @ 原创发布在 CycleGen。未经许可,禁止转载。

喜欢()
评论 (0)
    热门搜索
    173 文章
    1 评论
    47 喜欢
    Top