兄弟姐妹包进口

问题描述 投票:105回答:10

我已经尝试阅读有关兄弟进口甚至package documentation的问题,但我还没有找到答案。

具有以下结构:

├── 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中完成,对吧?

python packages python-import siblings
10个回答
51
投票

七年后

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

  • Installing包(在virtualenv或不是)会给你你想要的,虽然我建议使用pip来做,而不是直接使用setuptools(并使用setup.cfg来存储元数据)
  • Using the -m flag和作为一个包运行也有效(但如果你想将你的工作目录转换为可安装的包,会变得有点尴尬)。
  • 对于测试,具体来说,pytest能够在这种情况下找到api包并为你处理sys.path hacks

所以这真的取决于你想做什么。但是,在你的情况下,因为看起来你的目标是在某个时候制作一个合适的包装,通过pip -e安装可能是你最好的选择,即使它还不完美。

老答案

正如其他地方已经说过的那样,可怕的事实是你必须做丑陋的黑客以允许从__main__模块从兄弟模块或父包中导入。这个问题在PEP 366中有详细说明。 PEP 3122试图以更合理的方式处理进口,但Guido拒绝了它的说法

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

(Qazxswpoi)

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

here

这里# 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])的父文件夹)。


0
投票

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

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

只要您的工作目录仍然是Python项目的根目录,这似乎非常有效。如果有人在真实的生产环境中部署它,那么听听它是否在那里工作也会很棒。


-3
投票

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

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

from sibling import some_classPython Sibling Import Example内你可以打电话:

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

81
投票

Tired on sys.path hacks?

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


Setup

起点是您提供的文件结构,包含在名为setuptools的文件夹中。

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 称为根文件夹,在我的示例中,它位于.

API.朋友

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

C:\tmp\test_imports\

test_one.朋友

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

尝试运行test_one:

from api.api import function_from_api

def test_function():
    print(function_from_api())

if __name__ == '__main__':
    test_function()

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

使用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

Steps

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

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 的内容将是*

setup.py

2)使用虚拟环境

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

  • 创建虚拟环境 from setuptools import setup, find_packages setup(name='myproject', version='1.0', packages=find_packages())
  • 激活虚拟环境 python -m venv venv(Linux,macOS)或source ./venv/bin/activate(Win)

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

制作并激活虚拟环境后,控制台应在括号中指定虚拟环境的名称

./venv/Scripts/activate

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

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

3)将项目pip安装在可编辑状态

使用. ├── 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] 安装顶级软件包myproject。诀窍是在安装时使用pip标志。这样,它以可编辑状态安装,对.py文件所做的所有编辑将自动包含在已安装的包中。

在根目录中,运行

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

您还可以看到它是使用pip install -e .安装的

pip freeze

4)将(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 添加到您的导入中

请注意,您必须仅将myproject.添加到无法正常工作的导入中。没有myproject.setup.py工作的进口仍然可以正常工作。请参阅下面的示例。


Test the solution

现在,让我们使用上面定义的pip install测试解决方案,并在下面定义api.py

test_one.朋友

test_one.py

运行测试

from myproject.api.api import function_from_api

def test_function():
    print(function_from_api())

if __name__ == '__main__':
    test_function()

*有关详细的setup.py示例,请参阅(venv) PS C:\tmp\test_imports> python .\myproject\tests\test_one.py I am the return value from api.api!

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


38
投票

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

tests

27
投票

除非有必要,否则你不需要也不应该破解# Path hack. import sys, os sys.path.insert(0, os.path.abspath('..')) ,在这种情况下它不是。使用:

sys.path

从项目目录运行:import api.api_key # in tests, examples

你可能应该在qazxsw poi中移动qazxsw poi(如果它们是apis单元测试)并运行python -m tests.test_one来运行所有测试(假设有tests)或api来运行python -m api.test

您也可以从__main__.py中移除python -m api.test.test_one(它不是Python包)并在安装了test_one的virtualenv中运行示例,例如,如果你有适当的__init__.py,在virtualenv中的examples将安装inplace api包。


8
投票

我还没有必要理解Pythonology,以便在没有兄弟/相对导入黑客的情况下看到在不相关项目之间共享代码的预期方式。直到那天,这是我的解决方案。对于pip install -e .apisetup.py导入东西,它看起来像:

examples

3
投票

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

tests

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

..\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

在这种情况下,您必须使用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 启动脚本(请注意,在这种情况下,您不得提供'.py'扩展名):

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

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

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

2
投票

TLDR

此方法不需要setuptools,路径入侵,其他命令行参数或在项目的每个文件中指定包的顶级。

只需在父目录中创建一个脚本,无论您调用的是'-m' argument,都可以从那里运行所有内容。有关进一步说明,请继续阅

说明

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

我之前提到的这个失败的原因是被调用的程序将他们的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 。发生这种情况时,被调用的脚本接受自己位于包的顶层,并拒绝识别兄弟目录中的脚本。

但是,目录顶层下的所有内容仍然会识别顶级下的ANYTHING ELSE。这意味着,要让兄弟目录中的文件相互识别/使用,您必须做的就是从父目录中的脚本调用它们。

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

__main__

__name__包含以下代码:

__main__

sib1 / call.py包含:

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

和sib2 / callsib.py包含:

Main.py

如果你重现这个例子,你会注意到调用import sib1.call as call def main(): call.Call() if __name__ == '__main__': main() 会导致“被调用”被打印,就像在import sib2.callsib as callsib def Call(): callsib.CallSib() if __name__ == '__main__': Call() 中定义的那样,即使def CallSib(): print("Got Called") if __name__ == '__main__': CallSib() 是通过Main.py调用的。但是,如果要直接调用sib2/callsib.py(在对导入进行适当更改之后),则会抛出异常。尽管它在其父目录中的脚本调用时仍然有效,但如果它认为自己位于包的顶层,它将无法工作。


1
投票

您需要查看如何在相关代码中编写import语句。如果sib2/callsib.py使用以下import语句:

sib1/call.py

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

最简单的方法是在没有任何黑客攻击的情况下支持(如你所说)将从顶级目录运行示例,如下所示:

sib1/call.py

1
投票

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

© www.soinside.com 2019 - 2024. All rights reserved.