兄弟姐妹包进口

zac*_*ill 149 python packages siblings python-import

我已经尝试阅读有关兄弟进口甚至 包文档的问题,但我还没有找到答案.

具有以下结构:

??? 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
Run Code Online (Sandbox Code Playgroud)

如何在脚本 examplestests目录从导入 api模块,并可以从命令行运行?

此外,我想避免sys.path.insert每个文件的丑陋黑客.当然这可以在Python中完成,对吧?

np8*_*np8 102

厌倦了sys.path hacks?

有很多sys.path.append可用的东西,但我找到了解决手头问题的另一种方法:setuptools.我不确定是否存在与此无关的边缘情况.以下是使用Python 3.6.5(Anaconda,conda 4.5.1),Windows 10计算机进行测试的.


建立

起点是您提供的文件结构,包含在一个名为的文件夹中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
Run Code Online (Sandbox Code Playgroud)

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

api.py

作为测试用例,让我们使用以下./api/api.py

def function_from_api():
    return 'I am the return value from api.api!'
Run Code Online (Sandbox Code Playgroud)

test_one.py

from api.api import function_from_api

def test_function():
    print(function_from_api())

if __name__ == '__main__':
    test_function()
Run Code Online (Sandbox Code Playgroud)

尝试运行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'
Run Code Online (Sandbox Code Playgroud)

尝试相对进口也不会工作:

使用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
Run Code Online (Sandbox Code Playgroud)

脚步

1)将setup.py文件创建到根级目录

setup.py将是*的内容

from setuptools import setup, find_packages

setup(name='myproject', version='1.0', packages=find_packages())
Run Code Online (Sandbox Code Playgroud)

2)使用虚拟环境

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

  • 创建虚拟环境
    • python -m venv venv
  • 激活虚拟环境
    • source ./venv/bin/activate(Linux)或./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>
Run Code Online (Sandbox Code Playgroud)

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

.
??? 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]
Run Code Online (Sandbox Code Playgroud)

3)将项目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
Run Code Online (Sandbox Code Playgroud)

4)添加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()
Run Code Online (Sandbox Code Playgroud)

运行测试

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

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

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

  • 有人对“ModuleNotFoundError”有疑问吗?我已按照以下步骤将“myproject”安装到 virtualenv 中,当我进入解释会话并运行“import myproject”时,我收到“ModuleNotFoundError:没有名为“myproject”的模块”?`pip 列表已安装 | grep myproject`显示它在那里,目录是正确的,并且`pip`和`python`的版本都被验证是正确的。 (17认同)
  • 我认为令人作呕的是,我必须来到 stackoverflow 才能找到如何正确进行相对导入的真正答案。有关这些内容的文档确实需要改进。 (15认同)
  • 花了大约 2 个小时试图找出如何使相对导入起作用,而这个答案最终实际上做了一些明智的事情。 (5认同)
  • 感谢您的详细帖子。这是我的问题。如果我按照您说的做了所有事情并且冻结了点子,我会看到一行-e git + https://username@bitbucket.org/folder/myproject.git@f65466656XXXXX#egg=myproject ? (3认同)
  • 要解决这个问题,至少对我来说,请确保包含所有代码的目录与您在“setup.py”中的名称相同 - 对我来说,这是我的“src”目录。 (3认同)
  • 为什么相对导入解决方案不起作用?我相信你,但我试图理解 Python 的复杂系统。 (2认同)
  • 嘿@np8,它有效,我不小心将它安装在 venv 和 os 中:) `pip list` 显示包,而 `pip freeze` 如果使用标志 **-e** 安装则显示奇怪的名称 (2认同)
  • @ThoseKind我也遇到了同样的问题,“pip freeze”与你有相同的输出。仔细阅读这个答案后,我发现我的目录结构是错误的。请注意这个anwser的目录结构中的`.`,setup.py与`myproject`处于同一级别,而不是在`myproject`文件夹内,我错误地将setup.py放在`myproject`文件夹中,移动setup后.py 输出,一切正常。 (2认同)

Evp*_*pok 60

七年后

由于我在下面写了答案,修改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
Run Code Online (Sandbox Code Playgroud)

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

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

  • 也适用于path.append('..') (3认同)
  • 如果你[使用`-m`形式从项目目录运行或者你安装了软件包](http://stackoverflow.com/a/23542795/4279)(pip和virtualenv make it),你不必*简单) (3认同)
  • pytest如何为您找到api包?有趣的是,我发现了这个线程,因为我在使用pytest和同级包导入时遇到了这个问题。 (2认同)

Cen*_*lti 39

这是我在文件tests夹中的Python文件顶部插入的另一种替代方法:

# Path hack.
import sys, os
sys.path.insert(0, os.path.abspath('..'))
Run Code Online (Sandbox Code Playgroud)

  • 我认为新手(比如我自己)值得一提的是,这里的`..`是相对于你正在执行的目录---而不是包含该测试/示例文件的目录.我正在从项目目录执行,而我需要`./`.希望这有助于其他人. (7认同)
  • 这是一个糟糕的答案。破解路径并不是一个好的做法;它在 python 世界中的使用量是可耻的。这个问题的要点之一是了解如何在避免这种黑客攻击的同时进行导入。 (4认同)
  • `sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..'))` @JoshuaDetwiler (2认同)

jfs*_*jfs 28

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

import api.api_key # in tests, examples
Run Code Online (Sandbox Code Playgroud)

从项目目录运行: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 .的virtualenv将安装inplace apisetup.py.


s̮̦*_*̥̳̼ 11

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

\n

TL;DR:脚本(通常是入口点)只能执行import与其级别相同或低于其级别的任何内容。

\n

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

\n
MyProject\n\xe2\x94\x9c\xe2\x94\x80\xe2\x94\x80 src\n\xe2\x94\x82   \xe2\x94\x9c\xe2\x94\x80\xe2\x94\x80 bot\n\xe2\x94\x82   \xe2\x94\x82\xc2\xa0\xc2\xa0 \xe2\x94\x9c\xe2\x94\x80\xe2\x94\x80 __init__.py\n\xe2\x94\x82   \xe2\x94\x82\xc2\xa0\xc2\xa0 \xe2\x94\x9c\xe2\x94\x80\xe2\x94\x80 main.py\n\xe2\x94\x82   \xe2\x94\x82\xc2\xa0\xc2\xa0 \xe2\x94\x94\xe2\x94\x80\xe2\x94\x80 sib1.py\n\xe2\x94\x82   \xe2\x94\x94\xe2\x94\x80\xe2\x94\x80 mod\n\xe2\x94\x82       \xe2\x94\x9c\xe2\x94\x80\xe2\x94\x80 __init__.py\n\xe2\x94\x82       \xe2\x94\x94\xe2\x94\x80\xe2\x94\x80 module1.py\n\xe2\x94\x94\xe2\x94\x80\xe2\x94\x80 main.py\n\n
Run Code Online (Sandbox Code Playgroud)\n

要使用简单的命令从头开始运行我们的程序python main.py,我们在此处使用绝对导入(无前导点)main.py

\n
MyProject\n\xe2\x94\x9c\xe2\x94\x80\xe2\x94\x80 src\n\xe2\x94\x82   \xe2\x94\x9c\xe2\x94\x80\xe2\x94\x80 bot\n\xe2\x94\x82   \xe2\x94\x82\xc2\xa0\xc2\xa0 \xe2\x94\x9c\xe2\x94\x80\xe2\x94\x80 __init__.py\n\xe2\x94\x82   \xe2\x94\x82\xc2\xa0\xc2\xa0 \xe2\x94\x9c\xe2\x94\x80\xe2\x94\x80 main.py\n\xe2\x94\x82   \xe2\x94\x82\xc2\xa0\xc2\xa0 \xe2\x94\x94\xe2\x94\x80\xe2\x94\x80 sib1.py\n\xe2\x94\x82   \xe2\x94\x94\xe2\x94\x80\xe2\x94\x80 mod\n\xe2\x94\x82       \xe2\x94\x9c\xe2\x94\x80\xe2\x94\x80 __init__.py\n\xe2\x94\x82       \xe2\x94\x94\xe2\x94\x80\xe2\x94\x80 module1.py\n\xe2\x94\x94\xe2\x94\x80\xe2\x94\x80 main.py\n\n
Run Code Online (Sandbox Code Playgroud)\n

的内容bot/main.py利用显式相对导入来显示我们正在导入的内容,如下所示:

\n
from .sib1 import my_drink                # Both are explicit-relative-imports.\nfrom ..mod.module1 import relative_magic\n\ndef magic_tricks():\n    # Using sub-magic\n    relative_magic(in=["newbie", "pain"], advice="cheer_up")\n    \n    my_drink()\n    # Do your work\n    ...\n
Run Code Online (Sandbox Code Playgroud)\n

理由如下:

\n
    \n
  • 当我们真诚地想要运行我们的 Python 程序时,我们不想对“好吧,所以这是一个模块”感到害怕。\n
      \n
    • 所以我们使用绝对导入作为入口点main.py,这样我们就可以简单地运行我们的程序python main.py
    • \n
    • 在幕后,Python 将用来sys.path为我们解析包,但这也意味着我们想要导入的包可能会被任何其他同名包取代sys.path,因为例如 try中的路径顺序import test
    • \n
    \n
  • \n
  • 为了避免这些冲突,我们使用显式相对导入。\n
      \n
    • 语法from ..mod非常清楚地表明“我们正在导入我们自己的本地包”。
    • \n
    • 但缺点是,当你想将模块作为脚本运行时,你需要再次考虑“好吧,所以这是一个模块”。
    • \n
    \n
  • \n
  • 最后,该from ..mod 部分意味着它将上升一级到MyProject/src
  • \n
\n

结论

\n
    \n
  1. main.py脚本放在所有包的根目录旁边MyProject/src,并使用绝对导入python main.py来导入任何内容。没有人会创建一个名为src.
  2. \n
  3. 这些显式的相对导入将会起作用。
  4. \n
  5. 要运行模块,请使用python -m ....
  6. \n
\n

附录:有关将任何文件作为src/脚本运行的更多信息?

\n

那么你应该使用语法python -m 并看看我的另一篇文章:ModuleNotFoundError: No module named \'sib1\'

\n


小智 9

我还没有必要理解Pythonology,以便在没有兄弟/相对导入黑客的情况下看到在不相关项目之间共享代码的预期方式.直到那天,这是我的解决方案.对于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
Run Code Online (Sandbox Code Playgroud)


Pao*_*lli 5

对于兄弟包导入,您可以使用[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
Run Code Online (Sandbox Code Playgroud)

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

python examples/example_one.py
python tests/test_one.py
Run Code Online (Sandbox Code Playgroud)

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

if __name__ == '__main__' and if __package__ is not None:
    import ..api.api
Run Code Online (Sandbox Code Playgroud)

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

python -m packageName.examples.example_one
python -m packageName.tests.test_one
Run Code Online (Sandbox Code Playgroud)

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

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
Run Code Online (Sandbox Code Playgroud)