<h2>解释</h2>
<p>来自<a href="https://www.python.org/dev/peps/pep-0328/" rel="noreferrer">PEP 328</a></p>
<blockquote>
<p>Relative imports use a module's __name__ attribute to determine that
module's position in the package hierarchy. If the module's name does
not contain any package information (e.g. it is set to '__main__')
<strong>then relative imports are resolved as if the module were a top level
module</strong>, regardless of where the module is actually located on the file
system.</p>
</blockquote>
<p>在某个点上<a href="https://www.python.org/dev/peps/pep-0338/" rel="noreferrer">PEP 338</a>与<a href="https://www.python.org/dev/peps/pep-0328/" rel="noreferrer">PEP 328</a>冲突:</p>
<blockquote>
<p>... relative imports rely on <em>__name__</em> to determine the current
module's position in the package hierarchy. In a main module, the
value of <em>__name__</em> is always <em>'__main__'</em>, so explicit relative imports
will always fail (as they only work for a module inside a package)</p>
</blockquote>
<p>为了解决这个问题,<a href="https://www.python.org/dev/peps/pep-0366/" rel="noreferrer">PEP 366</a>引入了顶级变量<a href="https://docs.python.org/3/reference/import.html#__package__" rel="noreferrer">^{<cd1>}</a>:</p>
<blockquote>
<p>By adding a new module level attribute, this PEP allows relative
imports to work automatically if the module is executed using the <em>-m</em>
switch. A small amount of boilerplate in the module itself will allow
the relative imports to work when the file is executed by name. [...] When it [the attribute] is present, relative imports will be based on this attribute
rather than the module <em>__name__</em> attribute. [...] When the main module is specified by its filename, then the <em>__package__</em> attribute will be set to <em>None</em>. [...] <strong>When the import system encounters an explicit relative import in a
module without __package__ set (or with it set to None), it will
calculate and store the correct value</strong> (<strong>__name__.rpartition('.')[0]
for normal modules</strong> and <em>__name__</em> for package initialisation modules)</p>
</blockquote>
<p>(强调我的)</p>
<p>如果<code>__name__</code>是<code>'__main__'</code>,则<code>__name__.rpartition('.')[0]</code>返回空字符串。这就是为什么错误描述中有空字符串文字:</p>
<pre class="lang-none prettyprint-override"><code>SystemError: Parent module '' not loaded, cannot perform relative import
</code></pre>
<p>CPython的<a href="https://hg.python.org/cpython/file/9d65a195246b/Python/import.c#l1494" rel="noreferrer">^{<cd5>} function</a>的相关部分:</p>
<pre class="lang-c prettyprint-override"><code>if (PyDict_GetItem(interp->modules, package) == NULL) {
PyErr_Format(PyExc_SystemError,
"Parent module %R not loaded, cannot perform relative "
"import", package);
goto error;
}
</code></pre>
<p>如果在<code>interp->modules</code>(可作为<a href="https://docs.python.org/3/library/sys.html#sys.modules" rel="noreferrer">^{<cd8>}</a>访问)中找不到<code>package</code>(包的名称),CPython将引发此异常。由于<code>sys.modules</code>是一个将模块名映射到已加载“</em>的模块的字典,现在很明显,在执行相对导入之前,必须显式绝对导入父模块。</p>
<p><strong><em>注意:</em></strong>来自<a href="http://bugs.python.org/issue18018" rel="noreferrer">issue 18018</a>的修补程序添加了<a href="https://hg.python.org/cpython/file/c4e4886c6052/Python/import.c#l1494" rel="noreferrer">another ^{<cd10>} block</a>,将在</strong>上面的代码之前执行:</p>
<pre class="lang-c prettyprint-override"><code>if (PyUnicode_CompareWithASCIIString(package, "") == 0) {
PyErr_SetString(PyExc_ImportError,
"attempted relative import with no known parent package");
goto error;
} /* else if (PyDict_GetItem(interp->modules, package) == NULL) {
...
*/
</code></pre>
<p>如果<code>package</code>(同上)是空字符串,则错误消息将是</p>
<pre class="lang-none prettyprint-override"><code>ImportError: attempted relative import with no known parent package
</code></pre>
<p>但是,您只能在Python3.6或更新版本中看到这一点。</p>
<h2>解决方案1:使用-m运行脚本</h2>
<p>考虑一个目录(它是一个Python<a href="https://docs.python.org/3/glossary.html#term-package" rel="noreferrer">package</a>):</p>
<pre class="lang-none prettyprint-override"><code>.
├── package
│ ├── __init__.py
│ ├── module.py
│ └── standalone.py
</code></pre>
<p><em>包</em>中的所有文件都以相同的两行代码开头:</p>
<pre><code>from pathlib import Path
print('Running' if __name__ == '__main__' else 'Importing', Path(__file__).resolve())
</code></pre>
<p>我把这两条线包括进去,只是为了让手术的顺序更清楚。我们可以完全忽略它们,因为它们不会影响执行。</p>
<p><em>初始化py</em>和模块py</em>只包含这两行(即,它们实际上是空的)。</p>
<p><em>standalone.py</em>还尝试通过相对导入导入<em>module.py</em>:</p>
<pre><code>from . import module # explicit relative import
</code></pre>
<p>我们很清楚<code>/path/to/python/interpreter package/standalone.py</code>将失败。但是,我们可以使用<a href="https://docs.python.org/3/using/cmdline.html?highlight=#cmdoption-m" rel="noreferrer">^{<cd13>} command line option</a>运行该模块,该模块将<em>“搜索<a href="https://docs.python.org/3/library/sys.html#sys.path" rel="noreferrer">^{<cd14>}</a>指定模块,并将其内容作为<code>__main__</code>模块执行”</em>:</p>
<pre class="lang-bash prettyprint-override"><code>vaultah@base:~$ python3 -i -m package.standalone
Importing /home/vaultah/package/__init__.py
Running /home/vaultah/package/standalone.py
Importing /home/vaultah/package/module.py
>>> __file__
'/home/vaultah/package/standalone.py'
>>> __package__
'package'
>>> # The __package__ has been correctly set and module.py has been imported.
... # What's inside sys.modules?
... import sys
>>> sys.modules['__main__']
<module 'package.standalone' from '/home/vaultah/package/standalone.py'>
>>> sys.modules['package.module']
<module 'package.module' from '/home/vaultah/package/module.py'>
>>> sys.modules['package']
<module 'package' from '/home/vaultah/package/__init__.py'>
</code></pre>
<p><code>-m</code>为您执行所有导入操作并自动设置<code>__package__</code>,但是您可以在</p>
<h2>解决方案2:手动设置软件包</h2>
<p><strong><em>请将其视为概念的证明,而不是实际的解决方案。它不太适合在实际代码中使用。</em></strong></p>
<p><a href="https://www.python.org/dev/peps/pep-0366/" rel="noreferrer">PEP 366</a>有一个解决此问题的方法,但是它是不完整的,因为仅设置<code>__package__</code>是不够的。您需要导入模块层次结构中至少<em>N</em>前面的包,其中<em>N</em>是将搜索要导入的模块的父目录(相对于脚本目录)的数目。</p>
<p>因此</p>
<ol>
<li><p>将当前模块的前一个<em>Nth</em>的父目录添加到<code>sys.path</code></p></li>
<li><p>从<code>sys.path</code></p>中删除当前文件的目录</li>
<li><p>使用当前模块的完全限定名导入当前模块的父模块</p></li>
<li><p>将<code>__package__</code>设置为<em>2</em></p>中的完全限定名</li>
<li><p>执行相对导入</p></li>
</ol>
<p>我将从<em>解决方案1</em>中借用文件并添加更多子包:</p>
<pre class="lang-none prettyprint-override"><code>package
├── __init__.py
├── module.py
└── subpackage
├── __init__.py
└── subsubpackage
├── __init__.py
└── standalone.py
</code></pre>
<p>这次<em>standalone.py</em>将使用以下相对导入从<em>包</em>包导入<em>模块</p>
<pre><code>from ... import module # N = 3
</code></pre>
<p>我们需要在这一行前面加上样板代码,才能使其工作。</p>
<pre><code>import sys
from pathlib import Path
if __name__ == '__main__' and __package__ is None:
file = Path(__file__).resolve()
parent, top = file.parent, file.parents[3]
sys.path.append(str(top))
try:
sys.path.remove(str(parent))
except ValueError: # Already removed
pass
import package.subpackage.subsubpackage
__package__ = 'package.subpackage.subsubpackage'
from ... import module # N = 3
</code></pre>
<p>它允许我们按文件名执行<em>standalone.py</em>:</p>
<pre class="lang-bash prettyprint-override"><code>vaultah@base:~$ python3 package/subpackage/subsubpackage/standalone.py
Running /home/vaultah/package/subpackage/subsubpackage/standalone.py
Importing /home/vaultah/package/__init__.py
Importing /home/vaultah/package/subpackage/__init__.py
Importing /home/vaultah/package/subpackage/subsubpackage/__init__.py
Importing /home/vaultah/package/module.py
</code></pre>
<p>可以找到包装在函数中的更一般的解决方案<a href="https://gist.github.com/vaultah/d63cb4c86be2774377aa674b009f759a" rel="noreferrer">here</a>。示例用法:</p>
<pre><code>if __name__ == '__main__' and __package__ is None:
import_parents(level=3) # N = 3
from ... import module
from ...module.submodule import thing
</code></pre>
<h2>解决方案3:使用绝对导入和<a href="https://setuptools.readthedocs.io/en/latest/" rel="noreferrer">setuptools</a></h2>
<p>步骤是-</p>
<ol>
<li><p>将显式相对导入替换为等效的绝对导入</p></li>
<li><p>安装<code>package</code>使其可导入</p></li>
</ol>
<p>例如,目录结构可以如下</p>
<pre class="lang-none prettyprint-override"><code>.
├── project
│ ├── package
│ │ ├── __init__.py
│ │ ├── module.py
│ │ └── standalone.py
│ └── setup.py
</code></pre>
<p>其中<em>setup.py</em>是</p>
<pre><code>from setuptools import setup, find_packages
setup(
name = 'your_package_name',
packages = find_packages(),
)
</code></pre>
<p>T型其余文件是从<em>解决方案1</em>中借用的。</p>
<p>安装将允许您导入包,而不管您的工作目录如何(假设不会出现命名问题)。</p>
<p>我们可以修改<em>standalone.py</em>以使用此优势(步骤1):</p>
<pre><code>from package import module # absolute import
</code></pre>
<p>将工作目录更改为<code>project</code>,然后运行<code>/path/to/python/interpreter setup.py install --user</code>(<code>--user</code>在<a href="https://docs.python.org/3/library/site.html#site.USER_SITE" rel="noreferrer">your site-packages directory</a>中安装包)(步骤2):</p>
<pre class="lang-bash prettyprint-override"><code>vaultah@base:~$ cd project
vaultah@base:~/project$ python3 setup.py install --user
</code></pre>
<p>让我们验证现在是否可以将<em>standalone.py</em>作为脚本运行:</p>
<pre class="lang-bash prettyprint-override"><code>vaultah@base:~/project$ python3 -i package/standalone.py
Running /home/vaultah/project/package/standalone.py
Importing /home/vaultah/.local/lib/python3.6/site-packages/your_package_name-0.0.0-py3.6.egg/package/__init__.py
Importing /home/vaultah/.local/lib/python3.6/site-packages/your_package_name-0.0.0-py3.6.egg/package/module.py
>>> module
<module 'package.module' from '/home/vaultah/.local/lib/python3.6/site-packages/your_package_name-0.0.0-py3.6.egg/package/module.py'>
>>> import sys
>>> sys.modules['package']
<module 'package' from '/home/vaultah/.local/lib/python3.6/site-packages/your_package_name-0.0.0-py3.6.egg/package/__init__.py'>
>>> sys.modules['package.module']
<module 'package.module' from '/home/vaultah/.local/lib/python3.6/site-packages/your_package_name-0.0.0-py3.6.egg/package/module.py'>
</code></pre>
<p><strong><em>注意:如果您决定走这条路,最好使用<a href="https://docs.python.org/3/library/venv.html" rel="noreferrer">virtual environments</a>单独安装软件包。</p>
<h2>解决方案4:使用绝对导入和一些样板代码</h2>
<p>坦率地说,安装是不必要的-您可以添加一些样板代码到您的脚本,使绝对导入工作。</p>
<p>我将从<em>解决方案1</em>中借用文件,并更改<em>standalone.py</em>:</p>
<ol>
<li><p>在尝试使用绝对导入从<em>package</em>导入任何内容之前,将<em>package</em>的父目录添加到<code>sys.path</code><em>:</p>
<pre><code>import sys
from pathlib import Path # if you haven't already done so
file = Path(__file__).resolve()
parent, root = file.parent, file.parents[1]
sys.path.append(str(root))
# Additionally remove the current file's directory from sys.path
try:
sys.path.remove(str(parent))
except ValueError: # Already removed
pass
</code></pre></li>
<li><p>用绝对导入替换相对导入:</p>
<pre><code>from package import module # absolute import
</code></pre></li>
</ol>
<p><em>standalone.py</em>运行无问题:</p>
<pre class="lang-bash prettyprint-override"><code>vaultah@base:~$ python3 -i package/standalone.py
Running /home/vaultah/package/standalone.py
Importing /home/vaultah/package/__init__.py
Importing /home/vaultah/package/module.py
>>> module
<module 'package.module' from '/home/vaultah/package/module.py'>
>>> import sys
>>> sys.modules['package']
<module 'package' from '/home/vaultah/package/__init__.py'>
>>> sys.modules['package.module']
<module 'package.module' from '/home/vaultah/package/module.py'>
</code></pre>
<p>我觉得我应该警告你:尽量不要这样做,<em>特别是</em>如果你的项目有一个复杂的结构。</p>
<hr/>
<p>顺便说一句,<a href="https://www.python.org/dev/peps/pep-0008/#imports" rel="noreferrer">PEP 8</a>建议使用绝对导入,但声明在某些情况下可以接受显式相对导入:</p>
<blockquote>
<p>Absolute imports are recommended, as they are usually more readable
and tend to be better behaved (or at least give better error
messages). [...] However, explicit relative imports are an acceptable
alternative to absolute imports, especially when dealing with complex
package layouts where using absolute imports would be unnecessarily
verbose.</p>
</blockquote>