使用ProtoBuf进行相对导入:使用ProtoBuf生成Python类会导致ModuleNotFoundError错误。

22

Protobuf是否支持Python的相对导入?

我一直在尝试创建一个支持此功能的protobuf构建脚本。从我的.proto文件生成python类时,只有在启动python程序的目录与生成的.py文件所在目录相同时,才能成功导入python模块。

我已经构建了以下最小可行方案。理想情况下,我希望生成的python代码放置在单独的文件夹中(例如./generated),然后将其移动到其他项目中。我已发布了我能够使用的方法,但我希望有更多经验的人能够指向更好的解决方案。

常规信息:

  • Python 3.6.8
  • protobuf 3.11.3

文件夹结构:

.
|--- schemas
     |---- main_class.proto
     |---- sub
           |----sub_class.proto
|--- generated

尝试 1:相对导入

main_class.proto:

syntax = "proto3";

import public "sub/sub_class.proto";

message MainClass {
    repeated SubClass subclass = 1;
}

sub_class.proto:

syntax = "proto3";

message LogMessage {
    enum Status {
        STATUS_ERROR = 0;
        STATUS_OK = 1;
    }

    Status status = 1;
    string timestamp = 2;
}

message SubClass {
    string name = 1;
    repeated LogMessage log = 2;
}

Protoc 命令:

从根文件夹开始:

protoc -I=schemas --python_out=generated main_class.proto sub/sub_class.proto

这将把 python 文件放到 ./generated 文件夹中。

哪些可以使用,哪些不行

使用以上方法,我可以在文件夹 ./generated 中启动 python 并使用

import main_class_pb2 as MC_proto 导入。

然而,当我从 . 根文件夹(或任何其他文件夹)启动 python 时,使用

import generated.main_class_pb2 as MC_proto

会出现错误 ModuleNotFoundError: No module named 'sub'。基于 此帖子,我手动修改了生成的 main_class_pb2.py 文件,如下所示:

# Original
# from sub import sub_class_pb2 as sub_dot_sub__class__pb2
# from sub.sub_class_pb2 import *

# Fix
from .sub import sub_class_pb2 as sub_dot_sub__class__pb2
from .sub.sub_class_pb2 import *
通过在导入语句的开头添加 . ,我现在能够使用 import generated.main_class_pb2 as MC_proto 从根文件夹导入模块。但是,每次都手动编辑生成的文件非常不实用,所以我不喜欢这种方法。
尝试2:绝对导入
我的第二个尝试是尝试绝对导入。如果我知道我的项目根文件夹在哪里,我就可以将.proto文件移动到想要Python类的位置并在那里生成它们。对于此示例,我使用与之前相同的文件夹结构,但没有 ./generated 文件夹。我还必须更改protoc命令的根文件夹,并相应修改 main_class.proto 文件中的导入语句,如下所示:
syntax = "proto3";

// Old
//import public "sub/sub_class.proto";
// New
import public "schemas/sub/sub_class.proto";

message MainClass {
    repeated SubClass subclass = 1;
}

Protoc命令

protoc -I=. --python_out=. schemas/main_class.proto schemas/sub/sub_class.proto

哪些可行,哪些不可行

假设我的根文件夹也是我的项目根文件夹,这种方法现在允许我在根文件夹中启动python并使用以下方式导入该模块:

import schemas.main_class_pb2

但是,这意味着我的 .proto 文件必须位于该项目中与 Python 文件相同的文件夹中,这似乎非常混乱。这也意味着您必须从与项目相同的根文件夹生成 Python 文件,这并不总是可能的。.proto 文件可能用于为两个完全不同的应用程序创建公共接口,并且必须维护两个略有不同的 protobuf 项目似乎违背了使用 protobuf 的目的。


示例Python代码

我提供了一些示例Python代码,可用于测试导入是否有效以及类是否按预期工作。此示例来自尝试1,并假定Python是从./generated文件夹启动的。

import main_class_pb2 as MC_proto

sub1, sub2 = (MC_proto.SubClass(name='sub1'),
              MC_proto.SubClass(name='sub2'))

sub1.log.append(MC_proto.LogMessage(status=1, timestamp='2020-01-01'))
sub1.log.append(MC_proto.LogMessage(status=0, timestamp='2020-01-01'))
sub2.log.append(MC_proto.LogMessage(status=1, timestamp='2020-01-02'))

main = MC_proto.MainClass(subclass=[sub1, sub2])
main
Out[]: 
subclass {
  name: "sub1"
  log {
    status: STATUS_OK
    timestamp: "2020-01-01"
  }
  log {
    timestamp: "2020-01-01"
  }
}
subclass {
  name: "sub2"
  log {
    status: STATUS_OK
    timestamp: "2020-01-02"
  }
}

1
只要生成的文件所在位置在您的 PYTHONPATH 中,它们所在的位置就不重要。PYTHONPATH 设置为什么? - Greg Krimer
我可以将模块复制到我正在使用的项目的文件夹结构中,所以我想我不需要添加任何内容到 PYTHONPATH。问题在于,在我的尝试1中(这是我最理想的方法),相对导入没有被 protoc 自动处理。有没有办法告诉 python-converter 我想要相对导入? - ViggoTW
你真的需要把生成的代码放在“generated”文件夹里吗?为什么不直接在“schemas”文件夹内生成呢? - Clément Jean
1个回答

3
无法告诉protoc在生成Python代码时使用相对导入。通过查看C++中的protoc源代码,很清楚它只能使用绝对导入。请参见下面的代码: src/google/protobuf/compiler/python/generator.cc -> 生成其他proto文件导入部分的代码片段
// Prints Python imports for all modules imported by |file|.
void Generator::PrintImports() const {
  for (int i = 0; i < file_->dependency_count(); ++i) {
    const std::string& filename = file_->dependency(i)->name();

    std::string module_name = ModuleName(filename);
    std::string module_alias = ModuleAlias(filename);
    if (ContainsPythonKeyword(module_name)) {
      // If the module path contains a Python keyword, we have to quote the
      // module name and import it using importlib. Otherwise the usual kind of
      // import statement would result in a syntax error from the presence of
      // the keyword.
      printer_->Print("import importlib\n");
      printer_->Print("$alias$ = importlib.import_module('$name$')\n", "alias",
                      module_alias, "name", module_name);
    } else {
      int last_dot_pos = module_name.rfind('.');
      std::string import_statement;
      if (last_dot_pos == std::string::npos) {
        // NOTE(petya): this is not tested as it would require a protocol buffer
        // outside of any package, and I don't think that is easily achievable.
        import_statement = "import " + module_name;
      } else {
        import_statement = "from " + module_name.substr(0, last_dot_pos) +
                           " import " + module_name.substr(last_dot_pos + 1);
      }
      printer_->Print("$statement$ as $alias$\n", "statement", import_statement,
                      "alias", module_alias);
    }

    CopyPublicDependenciesAliases(module_alias, file_->dependency(i));
  }
  printer_->Print("\n");

  // Print public imports.
  for (int i = 0; i < file_->public_dependency_count(); ++i) {
    std::string module_name = ModuleName(file_->public_dependency(i)->name());
    printer_->Print("from $module$ import *\n", "module", module_name);
  }
  printer_->Print("\n");
}

该函数使用 module_name 来生成你第一次尝试的以下片段:
from sub import sub_class_pb2 as sub_dot_sub__class__pb2

from sub.sub_class_pb2 import *

module_name 是来自下面的 函数 ModuleName

// Returns the Python module name expected for a given .proto filename.
std::string ModuleName(const std::string& filename) {
  std::string basename = StripProto(filename);
  ReplaceCharacters(&basename, "-", '_');
  ReplaceCharacters(&basename, "/", '.');
  return basename + "_pb2";
}

如您所见,该函数中没有标志或逻辑来生成相对导入。

在我看来,最好的方法是使用您的第二次尝试,但在不同的包上,然后可以从Python代码中导入它。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接