什么是沙漏导入以及为什么要在代码库中避免它们?

What are hourglass imports and why would they be avoided in a codebase?

我在 Python 代码库中看到 some commits 删除了 "hourglass imports." 我以前从未见过这个术语,而且我无法通过 [=27= 找到任何相关信息] 文档或网络搜索。

什么是沙漏导入,什么时候使用或不使用它们?我最好的猜测是删除它们会产生子模块 easier to find,但还有其他原因吗?

从其中一个链接提交中删除沙漏导入的示例更改:

diff --git a/tensorflow/contrib/slim/python/slim/nets/vgg.py b/tensorflow/contrib/slim/python/slim/nets/vgg.py
index 3c29767f2..d4eb43cbb 100644
--- a/tensorflow/contrib/slim/python/slim/nets/vgg.py
+++ b/tensorflow/contrib/slim/python/slim/nets/vgg.py
@@ -37,13 +37,20 @@ Usage:
 @@vgg_16
 @@vgg_19
 """
+
 from __future__ import absolute_import
 from __future__ import division
 from __future__ import print_function

-import tensorflow as tf
-
-slim = tf.contrib.slim
+from tensorflow.contrib import layers
+from tensorflow.contrib.framework.python.ops import arg_scope
+from tensorflow.contrib.layers.python.layers import layers as layers_lib
+from tensorflow.contrib.layers.python.layers import regularizers
+from tensorflow.contrib.layers.python.layers import utils
+from tensorflow.python.ops import array_ops
+from tensorflow.python.ops import init_ops
+from tensorflow.python.ops import nn_ops
+from tensorflow.python.ops import variable_scope


 def vgg_arg_scope(weight_decay=0.0005):

顶级 tensorflow __init__.py 从子模块导出符号。

# tensorflow/python/__init__.py
...
from tensorflow.python.ops.standard_ops import *
...
# tensorflow/python/ops/standard_ops.py
...
from tensorflow.python.ops.array_ops import *
from tensorflow.python.ops.check_ops import *
from tensorflow.python.ops.clip_ops import *
...

此处为 TensorFlow 贡献者 :wave:。我们使用术语沙漏 import 指的是从其他模块导入一堆东西的模块 模块并重新导出它们。你在你的作品中提供了一个很好的例子 问题。

我们关心这个的原因以及我们称之为的原因 沙漏都与构建图的形状有关。整体 沙漏模块的要点是许多用户将依赖它作为 一个方便的入口点。它本身取决于很多内部 符号。所以你的依赖图有很多边通过这个 一个节点,通过 hourglass:

的中心汇集

Diagram of a simple build graph with three end-user binaries
depending on :standard_ops, and :standard_ops depending on three
internal targets.

在现实世界中,沙漏会更宽更深 比这更重要的是,双方。最终用户可以定义依赖于 :standard_ops 和依赖于这些库的二进制文件,以及 内部操作本身可能具有依赖层。

这样做的问题在于,它很难廉价且正确地 重建以响应变化。如果我们改变:check_ops的一部分,那么 看起来 :standard_ops 需要重建,因为它的其中一个 依赖关系发生了变化。而且因为 :standard_ops 已经重建, 它的依赖性也必须如此。但是现在我们已经重新构建了所有最终用户 程序,即使他们甚至没有实际使用该功能 完全由 :check_ops 提供。我们说构建图 过于逼近 实际的依赖关系图。过度逼近是 声音——构建仍然是正确的——但它可能是浪费的。

这是像 TensorFlow 这样的大型代码库的问题,我们有很多 数以千计的测试,当您更改任何代码时,我们 运行 所有受影响的测试, 而且测试可能很昂贵。如果你估计“哪些测试是 受此变化影响?”是一个巨大的过度逼近,因为 沙漏依赖,你在测试上浪费了大量的计算能力, 并且您的开发人员还必须等待更长时间才能合并他们的更改。

您原始问题中的补丁显示了我们如何删除一个 沙漏依赖并重写客户端以直接指向那些 他们实际使用的构建图的部分:

Diagram of a more precise build graph, with edges from end-user
programs to just those targets that they actually need

这样,如果:check_ops改了,我们可以看到只需要 重新构建并重新测试一个客户端。

这有利也有弊。对于真正的最终用户,必须 直接导入大量内部构件很烦人。不太好API, 不如 import numpy as npimport tensorflow as tf 好。 此外,它公开了实现细节,使我们更难 移动这些模块。因此,出于这些原因,我们 仍然 向用户提供沙漏导入,既公开又在 Google 内。 但是,我们尝试我们自己的中使用沙漏导入 代码库。在我们自己的存储库中,重大更改不是问题, 因为如果我们想重命名某些东西,我们可以重命名它的所有客户 同时。我们有 工具来处理我们的构建 图 并且很乐意这样做,这是 大多数 Python 程序员不想担心。这些工具是 非常好,虽然 - 除了生成漂亮的可视化图表(如 上面)对于你的真实代码库,它们是一个强大的查询引擎的基础, 在那里你可以问系统问题,比如“目标是什么 transitively dependen on :foo are still 运行ning on Python 2 属于 到我的团队?”。当您的构建图更多时,这会更强大 精确。

TL;DR: 沙漏模块是一个将来自许多进口的东西捆绑在一起的模块 子模块并将它们暴露给许多客户端模块。我们避开他们 因为它过度逼近了构建图,这使得它更 运行 测试成本高,分析代码更难。