使用 ProtoBuf 进行相对导入:使用 ProtoBuf 生成 Python 类会出现 ModuleNotFoundError

问题描述 投票:0回答:2

protobuf 支持 python 的相对导入吗?

我未能成功创建支持此功能的 protobuf 构建脚本。当从我的 .proto 文件生成 python 类时,如果我从创建生成的 .py 文件的同一文件夹启动 python,我只能导入 python 模块。

我构建了以下 MVP。理想情况下,我想要一个结构,其中生成的 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;
}

子类.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 -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
文件中的 import 语句,如下所示:

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 -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,并假设从

./generated
文件夹

启动 python
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"
  }
}
python python-3.x protocol-buffers protobuf-python
2个回答
3
投票

在生成 Python 代码时,无法告诉

protoc
使用相对导入。检查 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";
}

如您所见,此函数中没有用于生成相对导入的标志或逻辑。

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


0
投票

生成 Python 代码时无法告诉 protoc 使用相对导入。

Google 在 Github 上这个备受讨论的问题中忽视了社区 7 年,因此创建了一个包来解决这个问题,称为 Protoletariat。正如自述文件中的示例所述,它相当容易使用:

    对这些文件运行
  1. protoc
    
    
$ mkdir out $ protoc \ --python_out=out \ --proto_path=directory/containing/protos thing1.proto thing2.proto

    在生成的代码上运行
  1. protol
    
    
$ protol \ --create-package \ --in-place \ --python-out out \ protoc --proto-path=directory/containing/protos thing1.proto thing2.proto
最后的差异应该如下所示:

-import thing2_pb2 as thing2__pb2 - +from . import thing2_pb2 as thing2__pb2
    
© www.soinside.com 2019 - 2024. All rights reserved.