18

インポートされたモジュールが純粋な Python モジュールではなくC 拡張からのものであるかどうかを Python から判断する正しいまたは最も堅牢な方法は何ですか? これは、たとえば、Python パッケージに純粋な Python 実装と C 実装の両方を含むモジュールがあり、実行時にどちらが使用されているかを確認できるようにする場合に便利です。

1 つの考えは、ファイル拡張子を調べることmodule.__file__ですが、すべてのファイル拡張子をチェックする必要があるかどうか、またこのアプローチが必ずしも最も信頼できるかどうかはわかりません。

4

5 に答える 5

25

tl;dr

十分にテストされた回答については、以下の「完璧を求めて」サブセクションを参照してください。

abarnertによる移植可能な C 拡張機能の識別に関わる微妙な分析に対する実用的な対比として、Stack Overflow Productions™ は...実際の答えを提示します。

C 拡張機能と非 C 拡張機能を確実に区別できる能力は非常に有用であり、これがなければ Python コミュニティは貧しくなります。実際の使用例は次のとおりです。

  • アプリケーションがフリーズし、 1 つのクロスプラットフォーム Python コードベースが複数のプラットフォーム固有の実行可能ファイルに変換されます。PyInstallerは、ここでの標準的な例です。C 拡張機能を特定することは、堅牢な凍結に不可欠です。凍結されているコードベースによってインポートされたモジュールが C 拡張である場合、その C 拡張によって推移的にリンクされたすべての外部共有ライブラリも、そのコードベースで凍結する必要があります。恥ずべき告白: 私は PyInstallerに貢献しています。
  • アプリケーションの最適化。ネイティブ マシン コード ( Cythonなど)に対して静的に、またはジャスト イン タイム方式 ( Numbaなど) で動的に行います。自明の理由から、Python オプティマイザーは、コンパイル済みの C 拡張機能とコンパイルされていない純粋な Python モジュールを必然的に区別します。
  • 依存関係の分析、エンド ユーザーに代わって外部共有ライブラリを検査します。私たちの場合、必須の依存関係 ( Numpy ) を分析して、並列化されていない共有ライブラリ (参照 BLAS 実装など) にリンクしているこの依存関係のローカル インストールを検出し、その場合にエンド ユーザーに通知します。なんで?制御できない依存関係の不適切なインストールが原因でアプリケーションのパフォーマンスが低下した場合、責任を負いたくないからです。悪いパフォーマンスはあなたのせいです、不運なユーザーです!
  • おそらく他の重要な低レベルのもの。もしかしてプロファイリング?

フリーズ、最適化、およびエンド ユーザーの苦情の最小化が有用であることは、誰もが認めるところです。したがって、C 拡張機能を特定することは役に立ちます。

意見の相違が深まる

abarnertの最後から 2 番目の結論にも同意しません。

このために誰もが思いついた最高のヒューリスティックは、inspectモジュールに実装されているものなので、それを使用するのが最善です。

いいえ。これについて誰もが思いついた最高のヒューリスティックは、以下に示すものです。すべての stdlib モジュール (を含むがそれに限定されないinspect) は、この目的には役に立ちません。具体的には:

  • inspect.getsource()および関数は、C 拡張機能 (当然のことながら純粋な Python ソースを持たない) と、純粋な Python ソースを持たない他のタイプのモジュール (バイトコードのみのモジュールなど) の両方に対してinspect.getsourcefile()曖昧に返されます。役に立たないNone
  • importlibmachinesは、 PEP 302 準拠のローダーによってロード可能なモジュールにのみ適用されるため、デフォルトのインポート アルゴリズムに表示されます。便利ですが、一般的にはほとんど適用できません。PEP 302 に準拠しているという仮定は、現実の世界でパッケージが何度も直面すると崩れます。たとえば、ビルトインが実際にオーバーライド可能であることをご存知ですか? これは、Python のインポート メカニズムをカスタマイズするために使用した方法です。地球がまだ平らだった頃にさかのぼります。importlib__import__()

abarnert最終的な結論にも論争があります。

…完璧な答えはありません。

完璧な答えがあります。よく疑われるハイラルの伝説のトライフォースのように、すべての不完全な質問に対して完璧な答えが存在します。

見つけてみましょう。

完璧を求めて

次の純粋な Python 関数はTrue、渡された以前にインポートされたモジュール オブジェクトが C 拡張である場合にのみ返されます。簡単にするために、Python 3.xを想定しています。

import inspect, os
from importlib.machinery import ExtensionFileLoader, EXTENSION_SUFFIXES
from types import ModuleType

def is_c_extension(module: ModuleType) -> bool:
    '''
    `True` only if the passed module is a C extension implemented as a
    dynamically linked shared library specific to the current platform.

    Parameters
    ----------
    module : ModuleType
        Previously imported module object to be tested.

    Returns
    ----------
    bool
        `True` only if this module is a C extension.
    '''
    assert isinstance(module, ModuleType), '"{}" not a module.'.format(module)

    # If this module was loaded by a PEP 302-compliant CPython-specific loader
    # loading only C extensions, this module is a C extension.
    if isinstance(getattr(module, '__loader__', None), ExtensionFileLoader):
        return True

    # Else, fallback to filetype matching heuristics.
    #
    # Absolute path of the file defining this module.
    module_filename = inspect.getfile(module)

    # "."-prefixed filetype of this path if any or the empty string otherwise.
    module_filetype = os.path.splitext(module_filename)[1]

    # This module is only a C extension if this path's filetype is that of a
    # C extension specific to the current platform.
    return module_filetype in EXTENSION_SUFFIXES

長く見えるのは、docstring、コメント、アサーションが適切だからです。実際には 6 行しかありません。年寄りの心を食べ尽くせ、グイド。

プディングの証拠

4 つの移植可能なインポート可能なモジュールを使用して、この関数を単体テストしてみましょう。

  • stdlib 純粋な Pythonos.__init__モジュール。うまくいけば、C 拡張ではありません。
  • stdlib pure-Pythonimportlib.machineryサブモジュール。うまくいけば、C 拡張ではありません。
  • stdlib _elementtreeC 拡張。
  • サードパーティのnumpy.core.multiarrayC 拡張。

ウィット:

>>> import os
>>> import importlib.machinery as im
>>> import _elementtree as et
>>> import numpy.core.multiarray as ma
>>> for module in (os, im, et, ma):
...     print('Is "{}" a C extension? {}'.format(
...         module.__name__, is_c_extension(module)))
Is "os" a C extension? False
Is "importlib.machinery" a C extension? False
Is "_elementtree" a C extension? True
Is "numpy.core.multiarray" a C extension? True

すべては終わりです。

これを行う方法?

コードの詳細は重要ではありません。よし、どこから始めようか?

  1. 渡されたモジュールが PEP 302 準拠のローダーによってロードされた場合 (一般的なケース)、PEP 302 仕様では、このモジュールへのインポート時に割り当てられた属性が、このモジュールを__loader__ロードするローダー オブジェクトを値とする特別な属性を定義する必要があります。したがって:
    1. このモジュールのこの値が CPython 固有のimportlib.machinery.ExtensionFileLoaderクラスのインスタンスである場合、このモジュールは C 拡張です。
  2. それ以外の場合は、(A)アクティブな Python インタープリターが公式の CPython 実装 (例: PyPy )ではないか、 (B)アクティブな Python インタープリターは CPython ですが、このモジュールはPEP 302 準拠のローダーによってロードされませんでした。オーバーライドされている機械 (たとえば、この Python アプリケーションをプラットフォーム固有の凍結されたバイナリとして実行する低レベルのブートローダーによって)。いずれの場合も、このモジュールのファイルタイプが現在のプラットフォームに固有の C 拡張のファイルタイプであるかどうかのテストにフォールバックします。__import__()

8 行の機能と 20 ページの説明。それがまさに私たちが転がる方法です。

于 2016-09-03T07:02:11.360 に答える