兄弟包进口


199

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

具有以下结构:

├── 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

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

另外,我想避免sys.path.insert对每个文件进行难看的修改。当然可以在Python中完成,对吗?


7
我建议跳过所有sys.path技巧,并阅读迄今为止(7年后!)发布的唯一实际解决方案
阿兰·菲

1
顺便说一下,还有另一个好的解决方案的空间:将可执行代码与库代码分开;大部分时间一个包内的脚本不应该可执行的开始。
阿兰·菲

这对问题和答案都非常有帮助。我只是好奇,为什么“接受的答案”与在这种情况下获得赏金的人不一样?
Indominus

@ Aran-Fey在这些相对导入错误问答中,这被低估了。我一直以来都在寻找黑客手段,但是从深层次上,我知道有一种简单的方法可以解决问题。并不是说这是每个在这里阅读的人的解决方案,但这是对很多人的一个很好的提醒。
colorlace

Answers:


69

七年后

自从我在下面写下答案以来,修改sys.path仍然是一种快速技巧,对于私有脚本来说效果很好,但是已经有了一些改进

  • 安装该软件包(无论是否在virtualenv中)都将为您提供所需的内容,尽管我建议使用pip进行操作,而不是直接使用setuptools(并setup.cfg用于存储元数据)
  • 使用该-m标志并作为软件包运行也可以(但是如果要将工作目录转换为可安装的软件包,将会有些尴尬)。
  • 对于测试,特别是pytest能够在这种情况下找到api程序包,并sys.path为您解决问题

因此,这实际上取决于您要做什么。但是,就您而言,既然您的目标似乎是在某个时候制作一个合适的程序包,那么通过安装pip -e可能是您最好的选择,即使它尚不完美。

旧答案

正如其他地方已经说过的那样,可怕的事实是,您必须进行丑陋的修改才能允许从同级模块中导入数据或从该__main__模块中的父级程序包中进行导入。PEP 366中详细介绍了该问题。PEP 3122试图以更合理的方式处理进口,但圭多拒绝了它的一项

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

这里

不过,我会定期使用这种模式

# 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])顶级文件夹。

虽然我仍然不能使用相对导入,但是它确实允许从顶层(在您的示例api的父文件夹中)进行绝对导入。


3
你不,如果你使用从项目目录运行-m形式,或者如果你安装包(PIP和virtualenv中可以很容易)
JFS

2
pytest如何为您找到api包?有趣的是,我发现了这个线程,因为我在使用pytest和同级包导入时遇到了这个问题。
JuniorIncanter

1
请问两个问题。1.你的模式似乎不__package__ = "examples"适合我。为什么使用它?2.在什么情况下是,__name__ == "__main__"__package__不是None
Actual_panda19年

@actual_panda设置__packages__可以帮助您确定诸如examples.api使用iirc之类的绝对路径(但是自从我上次这样做以来已经有很长时间了),并且对于奇怪的情况和对将来的要求,检查该软件包是否不为None通常是一种故障安全措施。
Evpok

165

厌倦了sys.path hacks?

有大量的sys.path.append-hacks,但是我找到了另一种解决问题的方法。

摘要

  • 将代码包装到一个文件夹中(例如packaged_stuff
  • setup.py在使用setuptools.setup()的地方使用创建脚本。
  • 使用以下命令以可编辑状态安装软件包 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. 使用虚拟环境

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

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

要了解更多信息,只需在Google上搜索“ 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!

*有关更多详细的setup.py示例,请参阅setuptools文档

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


13
感谢您的详细帖子。这是我的问题。如果我按照您说的做了所有事情,并且冻结了一个点,我会收到一行-e git+https://username@bitbucket.org/folder/myproject.git@f65466656XXXXX#egg=myproject关于如何解决的任何想法?
星期一

2
相对导入解决方案为什么不起作用?我相信您,但是我想了解Python的复杂系统。
贾里德·尼尔森

8
有没有人对a有问题ModuleNotFoundError?按照以下步骤,我已经将'myproject'安装到virtualenv中,当我进入一个解释的会话并运行时,import myproject我得到了ModuleNotFoundError: No module named 'myproject'吗? pip list installed | grep myproject表明它的存在,该目录是正确的,并且两者的verison pippython被验证是正确的。
ThatKind

2
嘿@ np8,它可以工作,我不小心将它安装在venv和os中:) pip list显示软件包,而pip freeze如果用标志-e
Grzegorz Krug,

3
花了大约2个小时试图弄清楚如何使相对进口商品起作用,而这个答案实际上是最终做出了明智的选择。👍👍
格雷厄姆LEA

43

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

# Path hack.
import sys, os
sys.path.insert(0, os.path.abspath('..'))

1
+1确实很简单,而且效果很好。您需要将父类添加到导入中(例如api.api,examples.example_two),但我更喜欢这样做。
Evan Plaice 2012年

10
我认为值得向新手(像我一样)提及,..这是相对于您要从中执行的目录的,而不是包含该测试/示例文件的目录。我正在从项目目录执行,而我需要./。希望这对其他人有帮助。
Joshua Detwiler

@JoshDetwiler,绝对是。我没有意识到这一点。谢谢。
doak

1
这是一个很差的答案。破解路径不是一个好习惯;在python世界中使用了多少是可耻的。这个问题的主要要点之一就是看如何在避免这种黑客攻击的同时进行导入。
jtcotton63

sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..'))@JoshuaDetwiler
vldbnc

31

sys.path除非有必要,否则您不需要,也不应hack ,在这种情况下则没有必要。用:

import api.api_key # in tests, examples

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

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

您也可以__init__.pyexamples(不是Python软件包)中删除该示例,并在api安装了virtualenv的示例中运行示例,例如,如果您具有适当的条件,则pip install -e .在virtualenv中将安装就地api软件包setup.py


@Alex答案不假定测试是API测试,除非该段明确指出“如果它们是api的unittests”
jfs

不幸的是,您只能从根目录运行,PyCharm仍然找不到该文件的良好功能
mhstnsc

@mhstnsc:不正确。python -m api.test.test_one激活virtualenv后,您应该可以从任何地方运行。如果您无法将PyCharm配置为运行测试,请尝试提出一个新的Stack Overflow问题(如果在该主题上找不到现有问题)。
jfs

@jfs我错过了虚拟环境路径,但是我不想使用除shebang行之外的任何东西来从任何目录运行此东西。它不是与PyCharm一起运行。具有PyCharm的开发人员也知道他们已经完成并跳过了我无法使其与任何解决方案一起使用的功能。
mhstnsc

@mhstnsc适当的家当就足以在很多情况下(它指向的virtualenv蟒蛇二元任何像样的Python IDE应该支持的virtualenv。
JFS

9

我还没有对Python的理解,没有看到在没有同级/相对导入hack的情况下在不相关的项目之间共享代码的预期方式所必需的。直到那天,这是我的解决方案。对于examplestests从中导入东西..\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

这仍然会为您提供api父目录,并且您不需要“ / ..”串联sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(file))) )
卡米洛·桑切斯

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

我使用的是Click框架,它没有__file__全局sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(sys.argv[0]))))
变量,

3

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.pysib2/callsib.pysib1/call.pysib1/call.py


2

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

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

只要您的工作目录位于Python项目的根目录下,这似乎就非常有效。如果有人将其部署在实际的生产环境中,那么很高兴听到它是否也可以在此环境中工作。


仅当您从脚本的父目录运行时,此方法才有效
Evpok

1

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

import api.api

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

最简单的方法来支持此操作(如您所说),就是从顶层目录运行示例,如下所示:

PYTHONPATH=$PYTHONPATH:. python examples/example_one.py 

使用Python 2.7.1,我得到以下信息:$ python examples/example.py Traceback (most recent call last): File "examples/example.py", line 3, in <module> from api.api import API ImportError: No module named api.api。我也有同样的看法import api.api
zachwill 2011年

更新了我的答案...您确实必须将当前目录添加到导入路径,而无需解决。
AJ。

1

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


-3

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

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

里面examples或者tests您可以拨打:

from ..api import api

使用Python 2.7.1可获得以下信息:Traceback (most recent call last): File "example_one.py", line 3, in <module> from ..api import api ValueError: Attempted relative import in non-package
zachwill 2011年

2
哦,那么您应该将__init__.py文件添加到顶层目录。否则,Python无法将其视为模块

8
它不会工作。这个问题是不是父文件夹是不是一个包,它是因为该模块__name____main__不是package.module,Python不能看到它的父包,所以.点什么。
Evpok 2011年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.