簡體   English   中英

在 Python 中,如何判斷模塊是否來自 C 擴展?

[英]In Python how can one tell if a module comes from a C extension?

從 Python 判斷導入的模塊是否來自C 擴展而不是純 Python 模塊的正確或最可靠的方法是什么? 這很有用,例如,如果一個 Python 包有一個模塊同時具有純 Python 實現和 C 實現,並且您希望能夠在運行時判斷正在使用哪個模塊。

一個想法是檢查module.__file__的文件擴展名,但我不確定應該檢查所有文件擴展名,以及這種方法是否一定是最可靠的。

tl;博士

有關經過充分測試的答案,請參閱下面的“追求完美”小節。

作為對abarnert可移植識別 C 擴展所涉及的微妙之處的有用分析的實用對比,Stack Overflow Productions™ 提出了……一個實際的答案。

可靠地區分 C 擴展與非 C 擴展的能力非常有用,否則 Python 社區將變得貧困。 實際用例包括:

  • 應用程序凍結,將一個跨平台 Python 代碼庫轉換為多個特定於平台的可執行文件。 PyInstaller是這里的標准示例。 識別 C 擴展對於穩健凍結至關重要。 如果被凍結的代碼庫導入的模塊是 C 擴展,則由該 C 擴展可傳遞鏈接的所有外部共享庫也必須與該代碼庫一起凍結。 可恥的告白:我為 PyInstaller做出了貢獻
  • 應用程序優化,無論是靜態到本地機器代碼(例如Cython還是動態地以即時方式(例如Numba )。 出於不言而喻的原因,Python 優化器必須將已編譯的 C 擴展與未編譯的純 Python 模塊區分開來。
  • 依賴分析,代表最終用戶檢查外部共享庫。 我們的案例中,我們分析了一個強制依賴項 ( Numpy ) 以檢測此依賴項的本地安裝與非並行共享庫(例如,參考 BLAS 實現)的鏈接,並在出現這種情況時通知最終用戶。 為什么? 因為當我們的應用程序由於我們無法控制的依賴項安裝不當而導致性能不佳時,我們不希望受到指責。 糟糕的性能是你的錯,不幸的用戶!
  • 可能是其他必不可少的低級東西。 分析,也許?

我們都同意凍結、優化和盡量減少最終用戶的投訴是有用的。 因此,識別 C 擴展很有用。

分歧加深

我也不同意abarnert的倒數第二個結論:

任何人為此提出的最好的啟發式方法是在inspect模塊中實現的方法,所以最好的辦法就是使用它。

。任何人為此提出的最好的啟發式方法是下面給出的那些。 所有STDLIB模塊(包括但不限inspect )是無用用於這一目的。 具體來說:

  • inspect.getsource()inspect.getsourcefile()函數為 C 擴展(可以理解沒有純 Python 源)和其他類型的模塊也沒有純 Python 源(例如,僅字節碼的模塊inspect.getsourcefile()返回None )。 沒用
  • importlib機制適用於可通過符合 PEP 302 的加載程序加載的模塊,因此對默認的importlib導入算法可見。 有用,但幾乎不普遍適用。 當現實世界反復沖擊您的包裹時,PEP 302 合規性假設就失效了。 例如,您是否知道__import__()內置函數實際上是可覆蓋的 這就是我們用來自定義 Python 的導入機制的方式——回到地球還是平的時候。

abarnert最終結論也是有爭議的:

……沒有完美的答案。

有一個完美的答案。 就像經常被懷疑的海拉魯傳說中的三角力量一樣,每個不完美的問題都有一個完美的答案。

讓我們找到它。

追求完美

僅當傳遞的先前導入的模塊對象是 C 擴展時后面的純 Python 函數才返回True為簡單起見,假定為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

如果它看起來很長,那是因為文檔字符串、注釋和斷言都很好。 它實際上只有六行。 把你年邁的心吃掉,圭多。

布丁中的證據

讓我們用四個可移植的可導入模塊對這個函數進行單元測試:

  • stdlib 純 Python os.__init__模塊。 希望不是 C 擴展。
  • stdlib 純 Python importlib.machinery子模塊。 希望不是 C 擴展。
  • stdlib _elementtree C 擴展。
  • 第三方numpy.core.multiarray C 擴展。

即:

>>> 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 的加載器加載的,通常是由於默認__import__()機器被覆蓋(例如,由一個低級引導加載程序將此 Python 應用程序作為特定於平台的凍結二進制文件運行)。 在任何一種情況下,回退到測試此模塊的文件類型是否是特定於當前平台的 C 擴展的文件類型。

八行功能,二十頁解釋。 這就是我們如何滾動。

首先,我認為這根本沒有用。 模塊是 C 擴展模塊周圍的純 Python 包裝器是很常見的——或者,在某些情況下,C 擴展模塊周圍的純 Python 包裝器(如果可用)或純 Python 實現(如果沒有)。

對於一些流行的第三方示例: numpy是純 Python,即使所有重要的東西都是用 C 實現的; bintrees是純 Python,即使它的類都可以用 C 或 Python 實現,這取決於您如何構建它; 等。

這在 3.2 以后的大多數標准庫中都是正確的。 例如,如果您只import pickle ,則實現類將在 CPython cpickle C 構建(您曾經在 2.7 中從cpickle獲得),而它們將在 PyPy 中是純 Python 版本,但無論哪種方式, pickle本身都是純的蟒蛇。


但如果你真的想這樣做,你實際上需要區分件事:

  • 內置模塊,如sys
  • C 擴展模塊,如 2.x 的cpickle
  • 純 Python 模塊,如 2.x 的pickle

並且假設您只關心 CPython; 如果您的代碼在 Jython 或 IronPython 中運行,則實現可能是 JVM 或 .NET 而不是本機代碼。

由於多種原因,您無法根據__file__完美區分:

  • 內置模塊根本沒有__file__ (這在幾個地方都有記錄——例如, inspect文檔中的類型和成員表。)請注意,如果您使用的是py2appcx_freeze類的東西,那么算作“內置”的可能與獨立安裝不同.
  • 一個純 Python 模塊可能有一個 .pyc/.pyo 文件,而在分布式應用程序中沒有 .py 文件。
  • aa 包中的模塊作為單文件 egg 安裝(這在easy_install中很常見,在pip不太常見)將有一個空白或無用的__file__
  • 如果你構建一個二進制發行版,你的整個庫很可能會被打包到一個 zip 文件中,這會導致與單文件雞蛋相同的問題。

在 3.1+ 中,導入過程被大量清理,大部分用 Python 重寫,並且大部分暴露在 Python 層。

因此,您可以使用importlib模塊查看用於加載模塊的加載器鏈,最終您將獲得BuiltinImporter ( BuiltinImporter )、 ExtensionFileLoader (.so/.pyd/etc.)、 SourceFileLoader (.py)、或SourcelessFileLoader (.pyc/.pyo)。

您還可以在當前目標平台上看到分配給這四個中的每一個的后綴,作為importlib.machinery中的importlib.machinery 因此,您可以檢查any(pathname.endswith(suffix) for suffix in importlib.machinery.EXTENSION_SUFFIXES)) ,但這實際上並沒有幫助,例如,egg/zip 案例,除非您已經走過反正鏈。


任何人為此提出的最好的啟發式方法是在inspect模塊中實現的方法,所以最好的辦法就是使用它。

最佳選擇是getsourcegetsourcefilegetfile一個或多個; 哪個最好取決於您想要哪種啟發式方法。

內置模塊將為它們中的任何一個TypeError

擴展模塊應該為getsourcefile返回一個空字符串。 這似乎適用於我擁有的所有 2.5-3.4 版本,但我沒有 2.4。 對於getsource ,至少在某些版本中,它返回 .so 文件的實際字節,即使它應該返回空字符串或IOError (在 3.x 中,您幾乎肯定會得到UnicodeErrorSyntaxError ,但您可能不想依賴它……)

如果在 egg/zip/etc 中,純 Python 模塊可能會為getsourcefile返回一個空字符串。 如果源可用,它們應該始終為getsource返回一個非空字符串,即使在 egg/zip/etc. 中,但如果它們是無源字節碼(.pyc/etc.),它們將返回一個空字符串或引發 IOError .

最好的辦法是在您關心的發行版/設置中的您關心的平台上試驗您關心的版本。

@Cecil Curry 的功能非常出色。 兩個小評論:首先, _elementtree示例在我的 Python 3.5.6 副本中引發了TypeError 其次,正如@crld 指出的那樣,了解模塊是否包含C 擴展也很有幫助,但更便攜的版本可能會有所幫助。 因此,更通用的版本(使用 Python 3.6+ f-string 語法)可能是:

from importlib.machinery import ExtensionFileLoader, EXTENSION_SUFFIXES
import inspect
import logging
import os
import os.path
import pkgutil
from types import ModuleType
from typing import List

log = logging.getLogger(__name__)


def is_builtin_module(module: ModuleType) -> bool:
    """
    Is this module a built-in module, like ``os``?
    Method is as per :func:`inspect.getfile`.
    """
    return not hasattr(module, "__file__")


def is_module_a_package(module: ModuleType) -> bool:
    assert inspect.ismodule(module)
    return os.path.basename(inspect.getfile(module)) == "__init__.py"


def is_c_extension(module: ModuleType) -> bool:
    """
    Modified from
    https://stackoverflow.com/questions/20339053/in-python-how-can-one-tell-if-a-module-comes-from-a-c-extension.

    ``True`` only if the passed module is a C extension implemented as a
    dynamically linked shared library specific to the current platform.

    Args:
        module: Previously imported module object to be tested.

    Returns:
        bool: ``True`` only if this module is a C extension.

    Examples:

    .. code-block:: python

        from cardinal_pythonlib.modules import is_c_extension

        import os
        import _elementtree as et
        import numpy
        import numpy.core.multiarray as numpy_multiarray

        is_c_extension(os)  # False
        is_c_extension(numpy)  # False
        is_c_extension(et)  # False on my system (Python 3.5.6). True in the original example.
        is_c_extension(numpy_multiarray)  # True

    """  # noqa
    assert inspect.ismodule(module), f'"{module}" not a 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

    # If it's built-in, it's not a C extension.
    if is_builtin_module(module):
        return False

    # 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


def contains_c_extension(module: ModuleType,
                         import_all_submodules: bool = True,
                         include_external_imports: bool = False,
                         seen: List[ModuleType] = None,
                         verbose: bool = False) -> bool:
    """
    Extends :func:`is_c_extension` by asking: is this module, or any of its
    submodules, a C extension?

    Args:
        module: Previously imported module object to be tested.
        import_all_submodules: explicitly import all submodules of this module?
        include_external_imports: check modules in other packages that this
            module imports?
        seen: used internally for recursion (to deal with recursive modules);
            should be ``None`` when called by users
        verbose: show working via log?

    Returns:
        bool: ``True`` only if this module or one of its submodules is a C
        extension.

    Examples:

    .. code-block:: python

        import logging

        import _elementtree as et
        import os

        import arrow
        import alembic
        import django
        import numpy
        import numpy.core.multiarray as numpy_multiarray

        log = logging.getLogger(__name__)
        logging.basicConfig(level=logging.DEBUG)  # be verbose

        contains_c_extension(os)  # False
        contains_c_extension(et)  # False

        contains_c_extension(numpy)  # True -- different from is_c_extension()
        contains_c_extension(numpy_multiarray)  # True

        contains_c_extension(arrow)  # False

        contains_c_extension(alembic)  # False
        contains_c_extension(alembic, include_external_imports=True)  # True
        # ... this example shows that Alembic imports hashlib, which can import
        #     _hashlib, which is a C extension; however, that doesn't stop us (for
        #     example) installing Alembic on a machine with no C compiler

        contains_c_extension(django)

    """  # noqa
    assert inspect.ismodule(module), f'"{module}" not a module.'

    if seen is None:  # only true for the top-level call
        seen = []  # type: List[ModuleType]
    if module in seen:  # modules can "contain" themselves
        # already inspected; avoid infinite loops
        return False
    seen.append(module)

    # Check the thing we were asked about
    is_c_ext = is_c_extension(module)
    if verbose:
        log.info(f"Is module {module!r} a C extension? {is_c_ext}")
    if is_c_ext:
        return True
    if is_builtin_module(module):
        # built-in, therefore we stop searching it
        return False

    # Now check any children, in a couple of ways

    top_level_module = seen[0]
    top_path = os.path.dirname(top_level_module.__file__)

    # Recurse using dir(). This picks up modules that are automatically
    # imported by our top-level model. But it won't pick up all submodules;
    # try e.g. for django.
    for candidate_name in dir(module):
        candidate = getattr(module, candidate_name)
        # noinspection PyBroadException
        try:
            if not inspect.ismodule(candidate):
                # not a module
                continue
        except Exception:
            # e.g. a Django module that won't import until we configure its
            # settings
            log.error(f"Failed to test ismodule() status of {candidate!r}")
            continue
        if is_builtin_module(candidate):
            # built-in, therefore we stop searching it
            continue

        candidate_fname = getattr(candidate, "__file__")
        if not include_external_imports:
            if os.path.commonpath([top_path, candidate_fname]) != top_path:
                if verbose:
                    log.debug(f"Skipping, not within the top-level module's "
                              f"directory: {candidate!r}")
                continue
        # Recurse:
        if contains_c_extension(
                module=candidate,
                import_all_submodules=False,  # only done at the top level, below  # noqa
                include_external_imports=include_external_imports,
                seen=seen):
            return True

    if import_all_submodules:
        if not is_module_a_package(module):
            if verbose:
                log.debug(f"Top-level module is not a package: {module!r}")
            return False

        # Otherwise, for things like Django, we need to recurse in a different
        # way to scan everything.
        # See https://stackoverflow.com/questions/3365740/how-to-import-all-submodules.  # noqa
        log.debug(f"Walking path: {top_path!r}")
        try:
            for loader, module_name, is_pkg in pkgutil.walk_packages([top_path]):  # noqa
                if not is_pkg:
                    log.debug(f"Skipping, not a package: {module_name!r}")
                    continue
                log.debug(f"Manually importing: {module_name!r}")
                # noinspection PyBroadException
                try:
                    candidate = loader.find_module(module_name)\
                        .load_module(module_name)  # noqa
                except Exception:
                    # e.g. Alembic "autogenerate" gives: "ValueError: attempted
                    # relative import beyond top-level package"; or Django
                    # "django.core.exceptions.ImproperlyConfigured"
                    log.error(f"Package failed to import: {module_name!r}")
                    continue
                if contains_c_extension(
                        module=candidate,
                        import_all_submodules=False,  # only done at the top level  # noqa
                        include_external_imports=include_external_imports,
                        seen=seen):
                    return True
        except Exception:
            log.error("Unable to walk packages further; no C extensions "
                      "detected so far!")
            raise

    return False


# noinspection PyUnresolvedReferences,PyTypeChecker
def test() -> None:
    import _elementtree as et

    import arrow
    import alembic
    import django
    import django.conf
    import numpy
    import numpy.core.multiarray as numpy_multiarray

    log.info(f"contains_c_extension(os): "
             f"{contains_c_extension(os)}")  # False
    log.info(f"contains_c_extension(et): "
             f"{contains_c_extension(et)}")  # False

    log.info(f"is_c_extension(numpy): "
             f"{is_c_extension(numpy)}")  # False
    log.info(f"contains_c_extension(numpy): "
             f"{contains_c_extension(numpy)}")  # True
    log.info(f"contains_c_extension(numpy_multiarray): "
             f"{contains_c_extension(numpy_multiarray)}")  # True  # noqa

    log.info(f"contains_c_extension(arrow): "
             f"{contains_c_extension(arrow)}")  # False

    log.info(f"contains_c_extension(alembic): "
             f"{contains_c_extension(alembic)}")  # False
    log.info(f"contains_c_extension(alembic, include_external_imports=True): "
             f"{contains_c_extension(alembic, include_external_imports=True)}")  # True  # noqa
    # ... this example shows that Alembic imports hashlib, which can import
    #     _hashlib, which is a C extension; however, that doesn't stop us (for
    #     example) installing Alembic on a machine with no C compiler

    django.conf.settings.configure()
    log.info(f"contains_c_extension(django): "
             f"{contains_c_extension(django)}")  # False


if __name__ == '__main__':
    logging.basicConfig(level=logging.INFO)  # be verbose
    test()

雖然Cecil Curry 的回答有效(並且非常有用,我可能會補充說,它會為模塊的“頂級”返回 False,即使它包含使用 C 擴展的子模塊(例如 numpy 與 numpy .core.multiarray)。

雖然可能不那么健壯,但以下內容適用於我當前的用例:

def is_c(module):

    # if module is part of the main python library (e.g. os), it won't have a path
    try:
        for path, subdirs, files in os.walk(module.__path__[0]):

            for f in files:
                ftype = f.split('.')[-1]
                if ftype == 'so':
                    is_c = True
                    break
        return is_c

    except AttributeError:

        path = inspect.getfile(module)
        suffix = path.split('.')[-1]

        if suffix != 'so':

            return False

        elif suffix == 'so':

            return True

is_c(os), is_c(im), is_c(et), is_c_extension(ma), is_c(numpy)
# (False, False, True, True, True)

如果您和我一樣,看到了@Cecil Curry 的精彩回答和想法,如果沒有@Rudolf Cardinal 復雜的子庫遍歷,我怎么能以超級懶惰的方式對整個需求文件執行此操作,請不要再觀望了!

首先,將所有已安裝的要求(假設您在虛擬環境中執行此操作並且此處沒有其他內容)轉儲到pip freeze > requirements.txt的文件中。

然后運行以下腳本來檢查每個要求。

注意:這是非常懶惰的,對於許多導入名稱與其 pip 名稱不匹配的庫不起作用。

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

# function from Cecil Curry's answer:

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


with open('requirements.txt') as f:
    lines = f.readlines()
    for line in lines:
        # super lazy pip name to library name conversion
        # there is probably a better way to do this.
        lib = line.split("=")[0].replace("python-","").replace("-","_").lower()
        try:
            mod = importlib.import_module(lib)
            print(f"is {lib} a c extension? : {is_c_extension(mod)}")
        except:
            print(f"could not check {lib}, perhaps the name for imports is different?")

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM