Python包导入报错:ValueError: attempted relative import beyond top-level package
Python包导入报错:ValueError: attempted relative import beyond top-level package
前置知识:python -m命令
python -m
命令用于以模块的方式运行Python代码,而不是直接执行脚本文件。它为执行Python代码提供了一种更加灵活和标准化的方式,特别适用于包和模块。
python -m
的工作原理
当使用 python -m <module>
命令时,Python解释器会执行指定模块的代码,就像它被导入一样。这与直接运行脚本文件有一些不同,因为它会依赖模块的包结构来执行,而不是简单地从文件路径执行。
主要特点有:
- 以模块的方式运行:
python -m
会以模块的方式查找并执行代码,而不是直接从文件系统执行。它会确保按照 Python 的模块查找机制(依赖sys.path
)去定位模块,而不是像直接执行脚本时只关注当前路径。 - 运行包中的模块:你可以使用
python -m
来运行包中的子模块,甚至是安装在全局环境中的模块。
主要用途
1. 运行模块或包
python -m
最常见的用法是运行一个模块或包。例如,你可以用它来运行一个项目中的模块,而无需在项目根目录下执行脚本。
示例:
假设你的项目结构如下:
my_project/
my_package/
__init__.py
module.py
script.py
你可以使用以下命令运行 module.py
:
python -m my_package.module
好处:
- 通过
python -m
运行,Python会将当前工作目录作为顶层包目录,并确保以包的上下文运行模块。这避免了路径导入问题(如相对导入失效)。
2. 执行标准库模块的命令行接口
Python的一些标准库模块自带命令行接口,可以通过 python -m
运行。例如:
运行HTTP服务器:
python -m http.server
这将在当前目录下启动一个简单的HTTP服务器。
检查模块搜索路径:
你可以通过 python -m site
来查看 Python 的模块搜索路径、站点路径、site-packages
等信息:
python -m site
其他标准库命令:
python -m venv myenv
:创建虚拟环境。python -m unittest
:运行单元测试。python -m timeit
:运行性能测试。
3. 调试包中的代码
直接运行模块文件时,包结构中的相对导入可能会失败。通过 python -m
,可以确保相对导入能正确解析。例如,假设你的包中使用了相对导入,直接运行模块可能会报错:
python my_package/module.py
可能导致相对导入失效,报错类似:
ImportError: attempted relative import with no known parent package
使用 python -m
则可以避免这个问题:
python -m my_package.module
4. 调试已安装的第三方模块
如果你想调试已安装的模块,可以通过 python -m
运行。例如,运行Pip的命令行工具:
python -m pip install requests
这样做可以避免路径问题或错误的模块版本。
和直接运行脚本的区别
python script.py
- 查找路径:直接运行脚本时,Python将当前工作目录作为
sys.path
的一部分,并从这里查找模块。这有时会导致相对导入或包结构中的问题。 - 不依赖模块查找机制:直接执行脚本不考虑包结构,也不会处理包内相对导入问题。
python -m package.module
- 查找路径:
python -m
命令会通过包的路径查找模块,并依赖sys.path
。这使得相对导入和包结构能够正确处理。 - 处理包上下文:
python -m
确保在包的上下文中执行模块,解决了路径导入和包依赖问题。
运行测试文件的例子
在项目中,你可能会有以下结构:
my_project/
my_package/
__init__.py
module1.py
tests/
__init__.py
test_module1.py
你可以使用 python -m
来运行测试文件:
python -m tests.test_module1
总结
python -m
的主要功能是:
- 以模块方式运行代码:确保正确处理包结构、路径和相对导入。
- 运行标准库模块的命令行工具:许多Python标准库模块提供了命令行工具,能通过
python -m
来运行。 - 调试和测试包中的模块:尤其当相对导入或复杂的包结构可能引发错误时,
python -m
提供了更好的支持。
使用 python -m
可以让你在开发和调试复杂的Python包时更加灵活,避免路径和导入问题。
问题示例
package/
__init__.py
A/
__init__.py
foo.py
test_A/
__init__.py
test.py
如果在test_A当中,执行python -m test_A.test
,就会遇到"ValueError: attempted relative import beyond top-level package"
,但是在package当中就不会
简单解释
Why doesn't it work? It's because python doesn't record where a package was loaded from. So when you do python -m test_A.test, it basically just discards the knowledge that test_A.test is actually stored in package (i.e. package is not considered a package). Attempting from ..A import foo is trying to access information it doesn't have any more (i.e. sibling directories of a loaded location). It's conceptually similar to allowing from ..os import path in a file in math. This would be bad because you want the packages to be distinct. If they need to use something from another package, then they should refer to them globally with from os import path and let python work out where that is with $PATH
and $PYTHONPATH
When you use python -m package.test_A.test, then using from ..A import foo resolves just fine because it kept track of what's in package and you're just accessing a child directory of a loaded location.
Why doesn't python consider the current working directory to be a package? NO CLUE, but gosh it would be useful.
全解
Script vs. Module
Here's an explanation. The short version is that there is a big difference between directly running a Python file, and importing that file from somewhere else. Just knowing what directory a file is in does not determine what package Python thinks it is in. That depends, additionally, on how you load the file into Python (by running or by importing).
There are two ways to load a Python file: as the top-level script, or as a module. A file is loaded as the top-level script if you execute it directly, for instance by typing python myfile.py on the command line. It is loaded as a module when an import statement is encountered inside some other file. There can only be one top-level script at a time; the top-level script is the Python file you ran to start things off.
Naming
When a file is loaded, it is given a name (which is stored in its __name__
attribute). If it was loaded as the top-level script, its name is __main__
. If it was loaded as a module, its name is the filename, preceded by the names of any packages/subpackages of which it is a part, separated by dots.
So for instance in your example:
package/
__init__.py
subpackage1/
__init__.py
moduleX.py
moduleA.py
if you imported moduleX (note: imported, not directly executed), its name would be package.subpackage1.moduleX. If you imported moduleA, its name would be package.moduleA. However, if you directly run moduleX from the command line, its name will instead be __main__
, and if you directly run moduleA from the command line, its name will be __main__
. When a module is run as the top-level script, it loses its normal name and its name is instead __main__
.
Accessing a module NOT through its containing package
There is an additional wrinkle: the module's name depends on whether it was imported "directly" from the directory it is in or imported via a package. This only makes a difference if you run Python in a directory, and try to import a file in that same directory (or a subdirectory of it). For instance, if you start the Python interpreter in the directory package/subpackage1 and then do import moduleX, the name of moduleX will just be moduleX, and not package.subpackage1.moduleX. This is because Python adds the current directory to its search path when the interpreter is entered interactively; if it finds the to-be-imported module in the current directory, it will not know that that directory is part of a package, and the package information will not become part of the module's name.
A special case is if you run the interpreter interactively (e.g., just type python and start entering Python code on the fly). In this case, the name of that interactive session is __main__
.
Now here is the crucial thing for your error message: if a module's name has no dots, it is not considered to be part of a package. It doesn't matter where the file actually is on disk. All that matters is what its name is, and its name depends on how you loaded it.
Now look at the quote you included in your question:
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') then relative imports are resolved as if the module were a top-level module, regardless of where the module is actually located on the file system.
Relative imports...
Relative imports use the module's name to determine where it is in a package. When you use a relative import like from .. import foo, the dots indicate to step up some number of levels in the package hierarchy. For instance, if your current module's name is package.subpackage1.moduleX, then ..moduleA would mean package.moduleA. For a from .. import to work, the module's name must have at least as many dots as there are in the import statement.
... are only relative in a package
However, if your module's name is __main__
, it is not considered to be in a package. Its name has no dots, and therefore you cannot use from .. import statements inside it. If you try to do so, you will get the "relative-import in non-package" error.
Scripts can't import relative
What you probably did is you tried to run moduleX or the like from the command line. When you did this, its name was set to __main__
, which means that relative imports within it will fail, because its name does not reveal that it is in a package. Note that this will also happen if you run Python from the same directory where a module is, and then try to import that module, because, as described above, Python will find the module in the current directory "too early" without realizing it is part of a package.
Also remember that when you run the interactive interpreter, the "name" of that interactive session is always __main__
. Thus you cannot do relative imports directly from an interactive session. Relative imports are only for use within module files.
Two solutions:
If you really do want to run moduleX directly, but you still want it to be considered part of a package, you can do python -m package.subpackage1.moduleX. The -m tells Python to load it as a module, not as the top-level script.
Or perhaps you don't actually want to run moduleX, you just want to run some other script, say myfile.py, that uses functions inside moduleX. If that is the case, put myfile.py somewhere else – not inside the package directory – and run it. If inside myfile.py you do things like from package.moduleA import spam, it will work fine.
Notes
For either of these solutions, the package directory (package in your example) must be accessible from the Python module search path (sys.path). If it is not, you will not be able to use anything in the package reliably at all.
Since Python 2.6, the module's "name" for package-resolution purposes is determined not just by its __name__
attributes but also by the __package__
attribute. That's why I'm avoiding using the explicit symbol __name__
to refer to the module's "name". Since Python 2.6 a module's "name" is effectively __package__
+ '.' + __name__
, or just __name__
if __package__
is None.)