18

如果导入的模块来自C 扩展而不是纯 Python 模块,那么从 Python 中判断的正确或最可靠的方法是什么?这很有用,例如,如果一个 Python 包包含一个具有纯 Python 实现和 C 实现的模块,并且您希望能够在运行时判断正在使用哪个模块。

一个想法是检查 的文件扩展名module.__file__,但我不确定应该检查的所有文件扩展名,以及这种方法是否一定是最可靠的。

4

5 回答 5

25

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)都不能用于此目的。具体来说:

  • 对于C 扩展(可以理解,它们没有纯 Python 源代码)和其他类型的也没有纯 Python 源代码的模块(例如,仅字节码模块),inspect.getsource()andinspect.getsourcefile()函数会模棱两可地返回。没用None
  • importlibmachine适用于可由符合 PEP 302 的加载器加载的模块,因此对默认importlib导入算法可见。有用,但很难普遍适用。当现实世界反复撞击您的包裹时,PEP 302 合规性的假设就会失效。例如,您知道__import__()内置实际上是可覆盖的吗?这就是我们用来自定义 Python 导入机制的方式——回到地球还平坦的时候。

abarnert最终结论也是有争议的:

……没有完美的答案。

有一个完美的答案。就像经常被怀疑的 Hyrulean 传说中的Triforce一样,每个不完美的问题都有一个完美的答案。

让我们找到它。

追求完美

True仅当传递的先前导入的模块对象是 C 扩展时,后面的纯 Python 函数才会返回:为简单起见,假定为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 纯 Pythonos.__init__模块。希望不是 C 扩展。
  • stdlib 纯 Pythonimportlib.machinery子模块。希望不是 C 扩展。
  • 标准库_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 的加载器加载的,通常是由于默认__import__()机器被覆盖(例如,通过低级引导加载程序将此 Python 应用程序作为特定于平台的冻结二进制文件运行)。在任何一种情况下,都回退到测试该模块的文件类型是否是特定于当前平台的 C 扩展的文件类型。

八行功能,二十页解释。这就是我们滚动的方式。

于 2016-09-03T07:02:11.360 回答
16

首先,我认为这根本没有用。模块是围绕 C 扩展模块的纯 Python 包装器是很常见的——或者,在某些情况下,如果 C 扩展模块可用,则为纯 Python 包装器,或者如果不可用,则为纯 Python 实现。

对于一些流行的第三方示例:numpy是纯 Python,尽管所有重要的东西都是用 C 实现的;bintrees是纯 Python,尽管它的类可能全部用 C 或 Python 实现,具体取决于您如何构建它;等等

从 3.2 开始,大多数标准库都是如此。例如,如果你只是import pickle,实现类将cpickle在 CPython 中用 C (你过去在 2.7 中得到的)构建,而在 PyPy 中它们将是纯 Python 版本,但无论哪种方式pickle本身都是纯 Python。


但如果你确实想这样做,你实际上需要区分件事:

  • 内置模块,例如sys.
  • C 扩展模块,如 2.x 的cpickle.
  • 纯 Python 模块,如 2.x 的pickle.

这是假设你只关心 CPython;如果您的代码在 Jython 或 IronPython 中运行,则实现可能是 JVM 或 .NET,而不是本机代码。

您无法根据 完美区分__file__,原因有很多:

  • 内置模块根本没有__file__。(这在几个地方都有记录——例如,文档中的类型和成员inspect。)请注意,如果您使用类似py2appor的东西cx_freeze,那么算作“内置”的内容可能与独立安装不同。
  • 一个纯 Python 模块可能有一个 .pyc/.pyo 文件,而在分布式应用程序中没有一个 .py 文件。
  • aa 包中作为单文件 egg 安装的模块(easy_install与.pip__file__
  • 如果你构建一个二进制发行版,你的整个库很可能会打包在一个 zip 文件中,这会导致与单文件鸡蛋相同的问题。

在 3.1+ 中,导入过程已被大规模清理,大部分用 Python 重写,并且大部分暴露于 Python 层。

因此,您可以使用importlib模块查看用于加载模块的加载程序链,最终您将获得BuiltinImporter(builtins)、ExtensionFileLoader(.so/.pyd/etc.)、SourceFileLoader(.py) 或SourcelessFileLoader(.pyc ) /.pyo)。

您还可以在当前目标平台上查看分配给四个中每一个的后缀,作为importlib.machinery. 因此,您可以检查any(pathname.endswith(suffix) for suffix in importlib.machinery.EXTENSION_SUFFIXES)), 但这实际上并没有帮助,例如,鸡蛋/拉链盒,除非您已经沿着链条向上移动。


任何人为此想出的最好的启发式方法都是在inspect模块中实现的,所以最好的办法就是使用它。

最佳选择将是 、 和 中的一个getsourcegetsourcefile多个getfile;哪个最好取决于您想要哪种启发式方法。

内置模块将为其中TypeError任何一个提出 a 。

扩展模块应该为getsourcefile. 这似乎适用于我拥有的所有 2.5-3.4 版本,但我没有 2.4。对于getsource,至少在某些版本中,它返回 .so 文件的实际字节,即使它应该返回一个空字符串或引发一个 .so 文件IOError。(在 3.x 中,您几乎肯定会得到一个UnicodeErroror SyntaxError,但您可能不想依赖它……)

getsourcefile如果在 egg/zip/etc 中,纯 Python 模块可能会返回一个空字符串。如果源可用,即使在 egg/zip/etc. 中,它们也应该始终返回非空字符串getsource,但如果它们是无源字节码 (.pyc/etc.),它们将返回空字符串或引发 IOError。

最好的选择是在您关心的发行版/设置中在您关心的平台上试验您关心的版本。

于 2013-12-02T22:45:17.133 回答
3

@Cecil Curry 的功能非常出色。两个小评论:首先,该_elementtree示例引发了TypeError我的 Python 3.5.6 副本。其次,正如@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()
于 2018-09-22T15:23:50.767 回答
2

尽管Cecil Curry 的回答有效(并且信息量很大,就像abarnert's一样,我可能会补充)它会为模块的“顶级”返回 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)
于 2017-10-01T15:59:00.493 回答
0

如果您像我一样看到@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?")

于 2021-01-26T05:30:46.803 回答