312

我已经尝试阅读有关同级导入甚至 包文档的问题,但我还没有找到答案。

具有以下结构:

├── LICENSE.md
├── README.md
├── api
│   ├── __init__.py
│   ├── api.py
│   └── api_key.py
├── examples
│   ├── __init__.py
│   ├── example_one.py
│   └── example_two.py
└── tests
│   ├── __init__.py
│   └── test_one.py

examples和目录中的脚本如何 tests从模块导入 api并从命令行运行?

另外,我想避免sys.path.insert对每个文件进行丑陋的破解。这当然可以在 Python 中完成,对吧?

4

17 回答 17

300

厌倦了 sys.path 黑客?

有很多sys.path.append可用的-hacks,但我找到了解决手头问题的另一种方法。

概括

  • 将代码打包到一个文件夹中(例如packaged_stuff
  • setup.py在您使用setuptools.setup()的地方创建脚本。(见下文最小setup.py
  • Pip 以可编辑状态安装包pip install -e <myproject_folder>
  • 使用导入from packaged_stuff.modulename import function_name

设置

起点是您提供的文件结构,包装在一个名为myproject.

.
└── myproject
    ├── api
    │   ├── api_key.py
    │   ├── api.py
    │   └── __init__.py
    ├── examples
    │   ├── example_one.py
    │   ├── example_two.py
    │   └── __init__.py
    ├── LICENCE.md
    ├── README.md
    └── tests
        ├── __init__.py
        └── test_one.py

我将调用.根文件夹,在我的示例中,它位于C:\tmp\test_imports\.

api.py

作为测试用例,我们使用下面的 ./api/api.py

def function_from_api():
    return 'I am the return value from api.api!'

test_one.py

from api.api import function_from_api

def test_function():
    print(function_from_api())

if __name__ == '__main__':
    test_function()

尝试运行 test_one:

PS C:\tmp\test_imports> python .\myproject\tests\test_one.py
Traceback (most recent call last):
  File ".\myproject\tests\test_one.py", line 1, in <module>
    from api.api import function_from_api
ModuleNotFoundError: No module named 'api'

也尝试相对导入不起作用:

使用from ..api.api import function_from_api会导致

PS C:\tmp\test_imports> python .\myproject\tests\test_one.py
Traceback (most recent call last):
  File ".\tests\test_one.py", line 1, in <module>
    from ..api.api import function_from_api
ValueError: attempted relative import beyond top-level package

脚步

  1. 将 setup.py 文件添加到根目录

的内容setup.py是*

from setuptools import setup, find_packages

setup(name='myproject', version='1.0', packages=find_packages())
  1. 使用虚拟环境

如果您熟悉虚拟环境,请激活一个,然后跳到下一步。虚拟环境的使用不是绝对必要的,但从长远来看,它们确实会帮助你(当你有超过 1 个正在进行的项目时......)。最基本的步骤是(在根文件夹中运行)

  • 创建虚拟环境
    • python -m venv venv
  • 激活虚拟环境
    • source ./venv/bin/activate(Linux、macOS) 或./venv/Scripts/activate(Win)

要了解更多信息,只需谷歌搜索“python 虚拟环境教程”或类似内容。除了创建、激活和停用之外,您可能永远不需要任何其他命令。

一旦你创建并激活了一个虚拟环境,你的控制台应该在括号中给出虚拟环境的名称

PS C:\tmp\test_imports> python -m venv venv
PS C:\tmp\test_imports> .\venv\Scripts\activate
(venv) PS C:\tmp\test_imports>

你的文件夹树应该是这样的**

.
├── myproject
│   ├── api
│   │   ├── api_key.py
│   │   ├── api.py
│   │   └── __init__.py
│   ├── examples
│   │   ├── example_one.py
│   │   ├── example_two.py
│   │   └── __init__.py
│   ├── LICENCE.md
│   ├── README.md
│   └── tests
│       ├── __init__.py
│       └── test_one.py
├── setup.py
└── venv
    ├── Include
    ├── Lib
    ├── pyvenv.cfg
    └── Scripts [87 entries exceeds filelimit, not opening dir]
  1. pip 以可编辑状态安装您的项目

myproject使用pip. _ 诀窍是-e在安装时使用标志。这样它以可编辑状态安装,对 .py 文件所做的所有编辑都将自动包含在安装的包中。

在根目录下,运行

pip install -e .(注意点,它代表“当前目录”)

您还可以看到它是通过使用安装的pip freeze

(venv) PS C:\tmp\test_imports> pip install -e .
Obtaining file:///C:/tmp/test_imports
Installing collected packages: myproject
  Running setup.py develop for myproject
Successfully installed myproject
(venv) PS C:\tmp\test_imports> pip freeze
myproject==1.0
  1. 添加myproject.到您的导入中

请注意,您必须myproject.只添加到其他情况下不起作用的导入中。setup.py没有&的导入pip install仍然可以正常工作。请参阅下面的示例。


测试解决方案

现在,让我们使用api.py上面定义的和test_one.py下面定义的来测试解决方案。

test_one.py

from myproject.api.api import function_from_api

def test_function():
    print(function_from_api())

if __name__ == '__main__':
    test_function()

运行测试

(venv) PS C:\tmp\test_imports> python .\myproject\tests\test_one.py
I am the return value from api.api!

* 请参阅setuptools 文档以获取更详细的 setup.py 示例。

** 实际上,您可以将虚拟环境放在硬盘上的任何位置。

于 2018-05-05T20:52:59.100 回答
108

七年后

由于我在下面写了答案,因此修改sys.path仍然是一个快速而肮脏的技巧,适用于私有脚本,但已经有了一些改进

  • 安装包(无论是否在 virtualenv 中)都会给你你想要的,但我建议使用 pip 来完成它而不是直接使用 setuptools (并使用它setup.cfg来存储元数据)
  • 使用-m标志并作为包运行也可以(但如果您想将工作目录转换为可安装的包,会有点尴尬)。
  • 对于测试,具体来说,pytest能够在这种情况下找到 api 包并sys.path为您处理黑客攻击

所以这真的取决于你想做什么。但是,在您的情况下,由于您的目标似乎是在某个时候制作适当的软件包,因此安装pip -e可能是您最好的选择,即使它还不完美。

旧答案

正如在其他地方已经说过的那样,可怕的事实是,您必须进行丑陋的黑客攻击才能允许从同级模块或模块中的父包导入__main__。该问题在PEP 366中有详细说明。PEP 3122试图以更合理的方式处理进口,但 Guido 拒绝了它,原因是

唯一的用例似乎是运行恰好位于模块目录中的脚本,我一直将其视为反模式。

这里

虽然,我经常使用这种模式

# Ugly hack to allow absolute import from the root folder
# whatever its name is. Please forgive the heresy.
if __name__ == "__main__" and __package__ is None:
    from sys import path
    from os.path import dirname as dir

    path.append(dir(path[0]))
    __package__ = "examples"

import api

path[0]是您正在运行的脚本的父文件夹和dir(path[0])顶级文件夹。

尽管如此,我仍然无法使用相对导入,但它确实允许从顶层(在您的 exampleapi的父文件夹中)进行绝对导入。

于 2011-06-24T09:48:53.887 回答
49

tests这是我在文件夹中 Python 文件顶部插入的另一种选择:

# Path hack.
import sys, os
sys.path.insert(0, os.path.abspath('..'))
于 2012-02-25T17:02:56.163 回答
38

除非有必要,否则您不需要也不应该破解sys.path,在这种情况下不是。采用:

import api.api_key # in tests, examples

从项目目录运行:python -m tests.test_one.

您可能应该tests在里面移动(如果它们是 api 的单元测试)api并运行python -m api.test以运行所有测试(假设有__main__.py)或python -m api.test.test_one改为运行test_one

您还可以__init__.pyexamples(它不是 Python 包)中删除并在安装的 virtualenv 中运行示例,例如api,如果您有适当的.pip install -e .apisetup.py

于 2014-05-08T13:12:48.147 回答
10

我还没有对 Pythonology 的理解,无法在没有兄弟/相对导入黑客的情况下查看在不相关项目之间共享代码的预期方式。直到那天,这是我的解决方案。For examplesor teststo import stuff from ..\api,它看起来像:

import sys.path
import os.path
# Import from sibling directory ..\api
sys.path.append(os.path.dirname(os.path.abspath(__file__)) + "/..")
import api.api
import api.api_key
于 2012-04-12T20:32:45.277 回答
4

对于兄弟包导入,您可以使用[sys.path][2]模块的insertappend方法:

if __name__ == '__main__' and if __package__ is None:
    import sys
    from os import path
    sys.path.append( path.dirname( path.dirname( path.abspath(__file__) ) ) )
    import api

如果您按如下方式启动脚本,这将起作用:

python examples/example_one.py
python tests/test_one.py

另一方面,您也可以使用相对导入:

if __name__ == '__main__' and if __package__ is not None:
    import ..api.api

在这种情况下,您必须使用“-m”参数启动脚本(请注意,在这种情况下,您不能提供“.py”扩展名):

python -m packageName.examples.example_one
python -m packageName.tests.test_one

当然,您可以混合使用这两种方法,以便您的脚本无论如何调用都可以工作:

if __name__ == '__main__':
    if __package__ is None:
        import sys
        from os import path
        sys.path.append( path.dirname( path.dirname( path.abspath(__file__) ) ) )
        import api
    else:
        import ..api.api
于 2015-01-10T17:09:24.173 回答
2

TLDR

此方法不需要 setuptools、path hacks、额外的命令行参数或在项目的每个文件中指定包的顶层。

只需在您调用的任何内容的父目录中创建一个脚本,__main__然后从那里运行所有内容。如需进一步解释,请继续阅读。

解释

这可以在不一起破解新路径、额外的命令行参数或向每个程序中添加代码以识别其兄弟姐妹的情况下完成。

正如我相信之前提到的那样,失败的原因是被调用的程序将其__name__设置为__main__. 当这种情况发生时,被调用的脚本接受自己位于包的顶层并拒绝识别同级目录中的脚本。

但是,目录顶层下的所有内容仍将识别顶层下的任何其他内容。这意味着要让同级目录中的文件相互识别/利用,您唯一要做的就是从其父目录中的脚本调用它们。

概念证明 在具有以下结构的目录中:

.
|__Main.py
|
|__Siblings
   |
   |___sib1
   |   |
   |   |__call.py
   |
   |___sib2
       |
       |__callsib.py

Main.py包含以下代码:

import sib1.call as call


def main():
    call.Call()


if __name__ == '__main__':
    main()

sib1/call.py 包含:

import sib2.callsib as callsib


def Call():
    callsib.CallSib()


if __name__ == '__main__':
    Call()

sib2/callsib.py 包含:

def CallSib():
    print("Got Called")

if __name__ == '__main__':
    CallSib()

如果您重现此示例,您会注意到调用Main.py将导致打印“Got Called”,如中定义的那样,sib2/callsib.py 即使sib2/callsib.pygot call through也是如此sib1/call.py。但是,如果要直接调用sib1/call.py(在对导入进行适当更改之后),则会引发异常。即使它在其父目录中的脚本调用时有效,但如果它认为自己位于包的顶层,它将无法工作。

于 2018-11-06T17:11:06.120 回答
2

对于 2021 年的读者:如果您对以下内容没有信心pip install -e

考虑这个层次结构,正如Python 3 中相对导入的答案所建议的那样:

MyProject
├── src
│   ├── bot
│   │   ├── __init__.py
│   │   ├── main.py
│   │   └── sib1.py
│   └── mod
│       ├── __init__.py
│       └── module1.py
└── main.py

的内容main.py,这是起点,我们在这里使用绝对导入(无前导点):

from src.bot import main


if __name__ == '__main__':
    main.magic_tricks()

的内容bot/main.py,它利用了显式的相对导入

from .sib1 import my_drink                # Both are explicit-relative-imports.
from ..mod.module1 import relative_magic

def magic_tricks():
    # Using sub-magic
    relative_magic(in=["newbie", "pain"], advice="cheer_up")
    
    my_drink()
    # Do your work
    ...

现在推理来了:

  • 执行时python MyProject/main.pypath/to/MyProject被添加到sys.path.
  • 绝对导入import src.bot会读取它。
  • from ..mod 部分意味着它将上升一级至MyProject/src.
    • 我们能看到吗?的,因为path/to/MyProject被添加到sys.path.

所以重点是:

我们应该将主脚本放在旁边 MyProject/src,因为在进行相对引用时,我们不会离开src,而绝对导入为我们import src. 提供了恰到好处的范围:src/范围。

另请参阅:ModuleNotFoundError:没有名为“sib1”的模块

于 2021-09-08T08:11:50.413 回答
1

您需要查看相关代码中的导入语句是如何编写的。如果examples/example_one.py使用以下导入语句:

import api.api

...然后它期望项目的根目录位于系统路径中。

在没有任何 hack(如您所说)的情况下支持这一点的最简单方法是从顶级目录运行示例,如下所示:

PYTHONPATH=$PYTHONPATH:. python examples/example_one.py 
于 2011-06-12T18:47:03.990 回答
1

以防万一有人在 Eclipse 上使用 Pydev 到这里:您可以使用Project->Properties并在左侧菜单Pydev-PYTHONPATH下设置External Libraries将兄弟的父路径(以及调用模块的父路径)添加为外部库文件夹。然后你可以从你的兄弟姐妹那里导入,例如.from sibling import some_class

于 2014-05-07T19:05:39.680 回答
1

我做了一个示例项目来演示我是如何处理这个问题的,这确实是另一个 sys.path hack,如上所述。Python Sibling Import Example,它依赖于:

if __name__ == '__main__': import os import sys sys.path.append(os.getcwd())

只要您的工作目录保留在 Python 项目的根目录,这似乎非常有效。

于 2019-08-04T22:39:25.190 回答
1

如果您使用的是 pytest,那么pytest 文档描述了一种如何从单独的测试包中引用源包的方法。

建议的项目目录结构为:

setup.py
src/
    mypkg/
        __init__.py
        app.py
        view.py
tests/
    __init__.py
    foo/
        __init__.py
        test_view.py
    bar/
        __init__.py
        test_view.py

文件内容setup.py

from setuptools import setup, find_packages

setup(name="PACKAGENAME", packages=find_packages())

以可编辑模式安装软件包:

pip install -e .

pytest 文章引用了 Ionel Cristian Mărieş 的这篇博文

于 2021-03-18T14:47:04.667 回答
0

我想评论 np8 提供的解决方案,但我没有足够的声誉,所以我只想提一下,您可以完全按照他们的建议创建一个 setup.py 文件,然后pipenv install --dev -e .从项目根目录中将其转换为可编辑的依赖项。然后,您的绝对导入将起作用,例如from api.api import foo,您不必搞乱系统范围的安装。

文档

于 2020-10-25T00:34:03.610 回答
-1

在你的主文件中添加这个:

import sys
import os 
sys.path.append(os.path.abspath(os.path.join(__file__,mainScriptDepth)))

mainScriptDepth = 从项目根目录开始的主文件的深度。

这是你的情况 mainScriptDepth = "../../"from api.api import *然后,您可以通过从项目的根目录指定路径 ( ) 来导入。

于 2021-02-26T11:36:47.370 回答
-2

对于主要问题:

将兄弟文件夹称为模块:

从 .. 导入同级文件夹

从兄弟文件夹中调用 a_file.py 作为模块:

从 ..siblingfolder 导入 a_file

在同级文件夹中的文件中调用 a_function 作为模块:

从..siblingmodule.a_file 导入 func_name_exists_in_a_file

最简单的方法。

转到 lib/site-packages 文件夹。

如果存在“easy_install.pth”文件,只需对其进行编辑并添加您的目录,其中包含您想要将其作为模块的脚本。

如果不存在,只需制作一个...然后将您想要的文件夹放在那里

添加后...,python 将自动将该文件夹视为类似于站点包,您可以将该文件夹或子文件夹中的每个脚本作为模块调用。

我是用手机写的,很难设置让每个人都能舒服地阅读。

于 2020-08-21T05:03:26.587 回答
-3

首先,您应该避免使用与模块本身同名的文件。它可能会破坏其他进口。

导入文件时,解释器首先检查当前目录,然后搜索全局目录。

内部examplestests您可以致电:

from ..api import api
于 2011-06-12T19:06:10.233 回答
-3
  1. 项目

1.1用户

1.1.1 about.py

1.1.2初始化.py

1.2技术

1.2.1信息.py

1.1.2初始化.py

现在,如果您想从Tech 包中的info.py 模块访问User 包中的about.py 模块,那么您必须将 cmd(在 Windows 中)路径带到 Project 即 **C:\Users\Personal\Desktop \Project>**按照上面的包示例。并且从这个路径你要输入,python -m Package_name.module_name 例如对于上面我们要做的Package,

C:\Users\Personal\Desktop\Project>python -m Tech.info

小鬼点数

  1. 不要在info 模块后使用 .py 扩展名,即 python -m Tech.info.py
  2. 输入这个,兄弟包在同一级别。
  3. -m 是标志,要检查它,您可以从 cmd python --help键入
于 2020-08-03T15:52:29.930 回答