Pythonパッケージ内から(静的)ファイルを読み取る方法は?


106

Pythonパッケージ内のファイルをどのように読み取ることができますか?

私の状況

ロードするパッケージには、プログラム内からロードしたいテンプレート(文字列として使用されるテキストファイル)がいくつかあります。しかし、そのようなファイルへのパスをどのように指定しますか?

からファイルを読みたいと想像してください:

package\templates\temp_file

何らかのパス操作?パッケージの基本パス追跡?



回答:


-12

[2016年6月15日追加:明らかにこれはすべての状況で機能するわけではありません。他の回答を参照してください]


import os, mypackage
template = os.path.join(mypackage.__path__[0], 'templates', 'temp_file')

175

TLDR; 以下の方法2で説明するように、標準ライブラリのimportlib.resourcesモジュールを使用します。

伝統 pkg_resourcesからsetuptoolsもはや推奨されていない新しい方法理由は次のとおりです。

  • それははるかにパフォーマンス
  • (パススティングの代わりに)パッケージを使用するとコンパイル時エラーが発生するため、安全です。
  • パスを「結合」する必要がないため、より直感的です。
  • 余分な依存関係(setuptools)を必要としないため、開発時には高速ですが、Pythonの標準ライブラリのみに依存します。

既存のコードを移植するときの新しい方法との違いを説明するために、最初にリストした従来の方法をそのまま使用しました(移植もここで説明されています)。



テンプレートがモジュールのパッケージ内にネストされたフォルダーにあるとしましょう:

  <your-package>
    +--<module-asking-the-file>
    +--templates/
          +--temp_file                         <-- We want this file.

注1:確かに、__file__属性をいじらないでください(たとえば、zipから提供されるとコードが壊れます)。

注2:このパッケージをビルドする場合は、データファイルを内package_dataまたはdata_files内で宣言することを忘れないでくださいsetup.py

1)pkg_resourcesfrom setuptools(遅い)

setuptoolsディストリビューションのpkg_resourcesパッケージを使用することもできますが、それにはパフォーマンス面でのコストが伴います

import pkg_resources

# Could be any dot-separated package/module name or a "Requirement"
resource_package = __name__
resource_path = '/'.join(('templates', 'temp_file'))  # Do not use os.path.join()
template = pkg_resources.resource_string(resource_package, resource_path)
# or for a file-like stream:
template = pkg_resources.resource_stream(resource_package, resource_path)

チップ:

  • これは、あなたが設定することができて、あなたのディストリビューションは、zip形式の場合でも、データを読み込みます zip_safe=Trueあなたの中にsetup.py、および/または待望の使用zipappパッカーをからのpython-3.5自己完結型の分布を作成します。

  • setuptoolsランタイム要件に必ず追加してください(たとえば、install_requires`に)。

...そしてSetuptools / pkg_resourcesdocsによると、あなたは使用すべきではないことに注意してくださいos.path.join

基本的なリソースアクセス

リソース名は/-pathで区切られている必要があり、絶対パス(つまり、先頭に/)を付けたり、 " .."のような相対名を含めることはできません。リソースパスファイルシステムパスではないため、ルーチンを使用してリソースパスを操作しないでください。os.path

2)Python> = 3.7、またはバックポートされたimportlib_resourcesライブラリの使用

上記よりも効率的な標準ライブラリのimportlib.resourcesモジュールを使用しますsetuptools

try:
    import importlib.resources as pkg_resources
except ImportError:
    # Try backported to PY<37 `importlib_resources`.
    import importlib_resources as pkg_resources

from . import templates  # relative-import the *package* containing the templates

template = pkg_resources.read_text(templates, 'temp_file')
# or for a file-like stream:
template = pkg_resources.open_text(templates, 'temp_file')

注意:

機能についてread_text(package, resource)

  • package文字列またはモジュールのいずれかになります。
  • これresourceはもはやパスではなく、既存のパッケージ内で開くリソースのファイル名だけです。パスの区切り文字が含まれていない可能性があり、サブリソースがない可能性があります(つまり、ディレクトリにすることはできません)。

質問の例では、次のようにする必要があります。

  • 作る<your_package>/templates/ 空作成することによって、適切なパッケージに__init__.py、その中にファイルを、
  • これで、単純な(おそらく相対)importステートメントを使用できます(パッケージ/モジュール名の解析は不要になります)。
  • resource_name = "temp_file"(パスなし)を要求します。

チップ:

  • 現在のモジュール内のファイルにアクセスするには、パッケージの引数をに設定します(__package__たとえばpkg_resources.read_text(__package__, 'temp_file')、@ ben-maresに感謝します)。
  • 実際にファイル名がで尋ねられるとpath()、一時的に作成されたファイルにコンテキストマネージャが使用されるようになるため、状況は面白くなります(これを読んでください)。
  • 条件付きで古いPythonのバックポートされたライブラリを追加します(プロジェクトをでパッケージする場合はこれをinstall_requires=[" importlib_resources ; python_version<'3.7'"]チェックしてください)。setuptools<36.2.1
  • 従来の方法から移行した場合は、ランタイム要件setuptoolsからライブラリを削除することを忘れないでください。
  • カスタマイズするsetup.py、静的ファイルMANIFEST含めることを忘れないでください。
  • で設定することもできzip_safe=Trueますsetup.py

1
str.joinはシーケンスresource_path = '/'.join(('templates'、 'temp_file'))を取ります
Alex Punnen

私は入れませんNotImplementedError: Can't perform this operation for loaders without 'get_data()'任意のアイデア?
レオシェ

importlib.resourcespkg_resources必ずしも互換性ないことに注意してください。importlib.resourcesに追加されたzipファイルsys.path、setuptools、およびpkg_resourcesそれ自体がに追加されたディレクトリに格納されたzipファイルであるeggファイルを処理しますsys.path。例:sys.path = [..., '.../foo', '.../bar.zip']、卵はに入り.../fooますが、のパッケージbar.zipもインポートできます。を使用pkg_resourcesして、のパッケージからデータを抽出することはできませんbar.zip。setuptools importlib.resourcesがegg を操作するために必要なローダーを登録しているかどうかは確認していません。
Martijn Pieters

エラーPackage has no locationが表示された場合、追加のsetup.py構成が必要ですか?
zygimantus

1
現在のモジュール内のファイルにアクセスする場合(templates例のようにサブモジュールではなく)、package引数を__package__に設定できます。例:pkg_resources.read_text(__package__, 'temp_file')
Ben Mares

42

パッケージングの前奏:

リソースファイルの読み取りについて心配する前に、最初のステップは、最初にデータファイルがディストリビューションにパッケージ化されていることを確認することです。ソースツリーから直接読み取るのは簡単ですが、重要な部分はこれらのリソースファイルに、インストールされたパッケージ内のコードからアクセスできることを確認してください。

プロジェクトを次のように構成し、データファイルをパッケージのサブディレクトリに配置します。

.
├── package
   ├── __init__.py
   ├── templates
      └── temp_file
   ├── mymodule1.py
   └── mymodule2.py
├── README.rst
├── MANIFEST.in
└── setup.py

あなたは渡す必要include_package_data=Truesetup()コール。マニフェストファイルは、setuptools / distutilsを使用してソース配布をビルドする場合にのみ必要です。templates/temp_fileがこのサンプルプロジェクト構造に確実にパッケージ化されるようにするには、次のような行をマニフェストファイルに追加します。

recursive-include package *

過去の注意点: flit、poetryなどの最新のビルドバックエンドでは、マニフェストファイルを使用する必要はありません。これには、デフォルトでパッケージデータファイルが含まれます。したがって、使用pyproject.tomlしていてsetup.pyファイルがない場合は、に関するすべてのものを無視できますMANIFEST.in

さて、パッケージを邪魔にならないようにして、読み取り部分に...

勧告:

標準ライブラリpkgutilAPIを使用します。ライブラリコードでは次のようになります。

# within package/mymodule1.py, for example
import pkgutil

data = pkgutil.get_data(__name__, "templates/temp_file")
print("data:", repr(data))
text = pkgutil.get_data(__name__, "templates/temp_file").decode()
print("text:", repr(text))

ジップで動作します。Python 2とPython 3で動作します。サードパーティの依存関係は必要ありません。私はどんなマイナス面も実際には知りません(もしそうなら、答えにコメントしてください)。

避けるための悪い方法:

悪い方法#1:ソースファイルからの相対パスを使用する

これは現在受け入れられている答えです。よくても、次のようになります。

from pathlib import Path

resource_path = Path(__file__).parent / "templates"
data = resource_path.joinpath("temp_file").read_bytes()
print("data", repr(data))

それの何がいけないの?利用可能なファイルとサブディレクトリがあるという仮定は正しくありません。このアプローチは、zipまたはwheelにパックされたコードを実行する場合は機能せず、パッケージがファイルシステムに抽出されるかどうかにかかわらず、完全にユーザーの制御の及ばない可能性があります。

悪い方法#2:pkg_resources APIの使用

これは、トップ投票の回答で説明されています。次のようになります。

from pkg_resources import resource_string

data = resource_string(__name__, "templates/temp_file")
print("data", repr(data))

それの何がいけないの?実行時の依存関係をsetuptoolsに追加します。これは、インストール時の依存関係のみであることが望ましいです。自分のパッケージリソースにのみ関心があったpkg_resourcesとしても、コードはインストールされたすべてのパッケージのワーキングセットを構築するため、インポートと使用は非常に遅くなる可能性があります。これは、インストール時には(インストールは1回限りなので)大したことではありませんが、実行時には醜いです。

悪い方法#3:importlib.resources APIの使用

これは現在、トップ投票の回答の推奨事項です。これは最近の標準ライブラリの追加(Python 3.7の新機能)ですが、利用可能なバックポートもあります。次のようになります。

try:
    from importlib.resources import read_binary
    from importlib.resources import read_text
except ImportError:
    # Python 2.x backport
    from importlib_resources import read_binary
    from importlib_resources import read_text

data = read_binary("package.templates", "temp_file")
print("data", repr(data))
text = read_text("package.templates", "temp_file")
print("text", repr(text))

それの何がいけないの?まあ、残念ながら、それは動作しません...まだ。これはまだ不完全なAPI importlib.resourcesです。templates/__init__.pyデータファイルがサブディレクトリではなくサブパッケージ内に存在するようにするには、空のファイルを追加する必要があります。また、package/templatesサブディレクトリをインポート可能なpackage.templatesサブパッケージとして公開します。それが大した問題ではなく、気にならない場合は、先に進んで__init__.pyそこにファイルを追加し、インポートシステムを使用してリソースにアクセスできます。ただし、そのときは、my_resources.py代わりにファイルにして、モジュールでいくつかのバイト変数または文字列変数を定義してから、Pythonコードにインポートすることもできます。これは、どちらにせよここでの重労働を行うインポートシステムです。

サンプルプロジェクト:

私はgithubでサンプルプロジェクトを作成し、PyPIにアップロードしました。これは、上記の4つのアプローチすべてを示しています。それを試してみてください:

$ pip install resources-example
$ resources-example

詳細については、https://github.com/wimglenn/resources-exampleを参照してください


1
昨年5月に編集されました。しかし、私はイントロでの説明を見逃しがちです。それでも、あなたは標準に対して人々に助言します-それは噛むのが難しい弾丸です:-)
ankostis

1
@ankostis代わりに質問をさせてください。importlib.resourcesすでに非推奨となっている不完全なAPIのこれらすべての欠点にもかかわらず、なぜ推奨するのですか?新しいほど良いとは限りません。stdlib pkgutilと比べて実際にどのような利点があるか教えてください。答えには何も記載されていません。
WIM

1
親愛なる@wim、ブレットキヤノンの最後の応答の使用についてpkgutil.get_data()確認した私の直感で-それが-廃止されるAPI、発展途上です。そうは言っても、私はあなたに同意しますimportlib.resourcesが、はるかに優れた代替手段ではありませんが、PY3.10がこれを解決するまで、私はこの選択を支持します。
ankostis

1
@ankostisブレットのコメントを一粒の塩でとります。PEP 594-pkgutil廃止されたバッテリーを標準ライブラリーから削除することの非推奨スケジュールではまったく言及されておらず、正当な理由なしに削除されることはほとんどありません。Python 2.3以降で使用されており、PEP 302のローダープロトコルの一部として指定されています。「十分に定義されていないAPI」の使用は、Pythonの標準ライブラリの大部分を説明できるほど説得力のある回答ではありません。
WIM

2
追加させてくださいimportlibリソースも成功したいと思います!私はすべて、厳密に定義されたAPIを求めています。現状ではお勧めできません。APIはまだ変更中であり、既存の多くのパッケージでは使用できず、比較的最近のPythonリリースでのみ使用できます。実際にはpkgutil、あらゆる方法よりも悪いです。あなたの「直感」と権威への訴えは私には意味がありませんget_data。ローダーに問題がある場合は、証拠と実際的な例を示してください。
WIM

15

あなたがこの構造を持っている場合

lidtk
├── bin
   └── lidtk
├── lidtk
   ├── analysis
      ├── char_distribution.py
      └── create_cm.py
   ├── classifiers
      ├── char_dist_metric_train_test.py
      ├── char_features.py
      ├── cld2
         ├── cld2_preds.txt
         └── cld2wili.py
      ├── get_cld2.py
      ├── text_cat
         ├── __init__.py
         ├── README.md   <---------- say you want to get this
         └── textcat_ngram.py
      └── tfidf_features.py
   ├── data
      ├── __init__.py
      ├── create_ml_dataset.py
      ├── download_documents.py
      ├── language_utils.py
      ├── pickle_to_txt.py
      └── wili.py
   ├── __init__.py
   ├── get_predictions.py
   ├── languages.csv
   └── utils.py
├── README.md
├── setup.cfg
└── setup.py

このコードが必要です:

import pkg_resources

# __name__ in case you're within the package
# - otherwise it would be 'lidtk' in this example as it is the package name
path = 'classifiers/text_cat/README.md'  # always use slash
filepath = pkg_resources.resource_filename(__name__, path)

変な「常にスラッシュを使用する」部分はsetuptoolsAPIに由来します

また、Windowsを使用している場合でも、パスを使用する場合は、パスの区切り文字としてスラッシュ(/)を使用する必要があることに注意してください。Setuptoolsは、ビルド時にスラッシュを適切なプラットフォーム固有のセパレーターに自動的に変換します

ドキュメントがどこにあるのか疑問に思う場合:


簡潔な回答をありがとう
Paolo

8

答えを提供するDavid BeazleyおよびBrian K. JonesによるPython Cookbook、Third Editionの「10.8。パッケージ内のデータファイルの読み取り」の内容。

私はここにそれを取得します:

次のように編成されたファイルを含むパッケージがあるとします。

mypackage/
    __init__.py
    somedata.dat
    spam.py

次に、spam.pyファイルがsomedata.datファイルの内容を読み取りたいと仮定します。これを行うには、次のコードを使用します。

import pkgutil
data = pkgutil.get_data(__package__, 'somedata.dat')

結果の変数データは、ファイルの生の内容を含むバイト文字列になります。

get_data()の最初の引数は、パッケージ名を含む文字列です。直接指定するか、などの特殊変数を使用できます__package__。2番目の引数は、パッケージ内のファイルの相対名です。最終的なディレクトリがパッケージ内にある限り、必要に応じて、標準のUnixファイル名規則を使用して別のディレクトリに移動できます。

このように、パッケージはディレクトリ、.zipまたは.eggとしてインストールできます。



-2

eggファイルを使用していると仮定します。抽出されません:

私は最近のプロジェクトでこれを「解決」しました。postinstallスクリプトを使用して、テンプレートをegg(zipファイル)からファイルシステムの適切なディレクトリに抽出します。これは、__path__[0]ことは時々失敗可能でした(名前を思い出せませんが、少なくとも1つのライブラリを調べて、リストの前に何かを追加しました!)。

また、卵ファイルは通常、「卵キャッシュ」と呼ばれる一時的な場所にその場で抽出されます。スクリプトを開始する前または後で、環境変数を使用してその場所を変更できます。

os.environ['PYTHON_EGG_CACHE'] = path

ただし、正しく機能する可能性のあるpkg_resourcesがあります。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.