web-dev-qa-db-ja.com

兄弟パッケージのインポート

兄弟のインポートや パッケージドキュメント についての質問を読んでみましたが、まだ答えが見つかりません。

次の構造で:

├── 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モジュールからインポートし、コマンドラインから実行するにはどうすればよいですか?

また、すべてのファイルのfileいsys.path.insertハックを回避したいと思います。確かにこれはPythonで行うことができますよね?

133
zachwill

7年後

以下の回答を書いたので、sys.pathを変更することは、プライベートスクリプトでうまく機能する手っ取り早い方法ですが、いくつかの改善があります。

  • インストール パッケージ(virtualenvであるかどうかに関係なく)を使用すると、必要なものが得られますが、setuptoolsを直接使用する(およびsetup.cfgを使用してメタデータを保存するのではなく、pipを使用することをお勧めします) )
  • -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 __== "__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の親フォルダー)からの絶対インポートが可能です。

53
Evpok

Sys.pathハッキングにうんざりしていませんか?

利用可能なsys.path.append -hacksはたくさんありますが、問題を解決する別の方法を見つけました: setuptools 。これでうまく機能しないEdgeケースがあるかどうかはわかりません。以下は、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

.をルートフォルダーと呼びます。この例では、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 __== '__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())

2)仮想環境を使用する

仮想環境に精通している場合は、アクティブにし、次の手順にスキップします。仮想環境の使用は絶対に必要ですが、それらは本当に長期的にあなたを助けます(複数のプロジェクトがある場合)進行中..)。最も基本的な手順は次のとおりです(ルートフォルダーで実行)

  • 仮想環境を作成します
    • python -m venv venv
  • 仮想環境をアクティブにします
    • source ./venv/bin/activate(Linux、macOS)または./venv/Scripts/activate(Win)

詳細については、「python virtual env tutorial」などをGoogleで検索してください。おそらく、作成、アクティブ化、非アクティブ化以外のコマンドは必要ありません。

仮想環境を作成してアクティブにすると、コンソールには括弧内に仮想環境の名前が表示されます。

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]

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

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 __== '__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 docs を参照してください。

**実際には、ハードディスクのどこにでも仮想環境を置くことができます。

82
np8

testsフォルダーのPythonファイルの先頭に挿入する別の代替方法を次に示します。

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

必要でない限り、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を実行します。

examplesから__init__.py(Pythonパッケージではない)を削除し、apiがインストールされているvirtualenvでサンプルを実行することもできます(例:pip install -e .適切なsetup.pyがある場合、virtualenvでapiパッケージをインプレースでインストールします。

27
jfs

兄弟/相対的なインポートハックなしで無関係なプロジェクト間でコードを共有する意図された方法を見るために必要なPythonologyの理解がまだありません。その日まで、これが私の解決策です。 examplesまたは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
9
user1330131

兄弟パッケージのインポートでは、insertまたはappendのいずれかを使用できます[sys.path] [2]モジュールのメソッド:

if __== '__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 __== '__main__' and if __package__ is not None:
    import ..api.api

この場合、スクリプトを '-m'引数 で起動する必要があります(この場合、 '。py'を指定しないでください。 拡張子):

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

もちろん、2つのアプローチを組み合わせて、スクリプトがどのように呼び出されても機能するようにすることができます。

if __== '__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
3
Paolo Rovelli

TLDR

この方法では、setuptools、パスハック、追加のコマンドライン引数、またはプロジェクトのすべての単一ファイルでパッケージのトップレベルを指定する必要はありません。

__main__になるように呼び出しているものの親ディレクトリにスクリプトを作成し、そこからすべてを実行します。詳細については、読み続けてください。

説明

これは、新しいパスを一緒にハッキングしたり、追加のコマンドライン引数を追加したり、兄弟を認識するために各プログラムにコードを追加したりすることなく実現できます。

前に述べたように、これが失敗する理由は、呼び出されるプログラムの__name____main__に設定されているためです。これが発生すると、呼び出されるスクリプトはパッケージの最上位にあることを受け入れ、兄弟ディレクトリ内のスクリプトを認識しません。

ただし、ディレクトリの最上位の下にあるものはすべて、最上位の下にあるANYTHING ELSEを認識します。つまり、兄弟ディレクトリのファイルを取得して相互に認識/利用するためにのみを実行する必要があるのは、親ディレクトリのスクリプトからファイルを呼び出すことです。

概念実証次の構造のディレクトリ内:

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

Main.pyには次のコードが含まれます。

import sib1.call as call


def main():
    call.Call()


if __== '__main__':
    main()

sib1/call.pyの内容:

import sib2.callsib as callsib


def Call():
    callsib.CallSib()


if __== '__main__':
    Call()

sib2/callsib.pyには以下が含まれます。

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

if __== '__main__':
    CallSib()

この例を再現すると、Main.pyを介してsib2/callsib.pyが呼び出された場合でも、sib2/callsib.pyを呼び出すとsib1/call.pyで定義されているように「Got Called」が出力されます。ただし、sib1/call.pyを直接呼び出すと(インポートに適切な変更を加えた後)、例外がスローされます。親ディレクトリのスクリプトから呼び出されたときは動作していましたが、パッケージの最上位にあると考えている場合は動作しません。

2
Thunderwood

EclipseでPydevを使用している人がここにいる場合に備えて:Project-> Propertiesを使用して兄弟の親パス(および呼び出しモジュールの親)を外部ライブラリフォルダーとして追加できます外部ライブラリ左メニューPydev-PYTHONPATHの下。その後、兄弟からインポートできます。 g。 from sibling import some_class

1

インポートステートメントが関連するコードでどのように記述されているかを確認する必要があります。 examples/example_one.pyが次のインポートステートメントを使用する場合:

import api.api

...次に、プロジェクトのルートディレクトリがシステムパスにあると想定します。

ハックなしでこれをサポートする最も簡単な方法は、次のように最上位ディレクトリから例を実行することです。

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

これをどのように処理するかを示すためにサンプルプロジェクトを作成しました。これは、上記のsys.pathハックです。 Python兄弟インポートの例 、これは次のものに依存します。

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

作業ディレクトリがPythonプロジェクトのルートにある限り、これは非常に効果的であるようです。誰かが実際の実稼働環境でこれを展開する場合、それがそこでも動作するかどうかを聞くのは素晴らしいことです。

0
ADataGMan