在Python中,如何判断模块是否来自C扩展?

时间:2013-12-02 22:18:04

标签: python python-c-extension extension-modules

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

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

5 个答案:

答案 0 :(得分:15)

  

<强> TL;博士

     

参见&#34;寻求完美&#34;以下小节为经过充分测试的答案。

作为对abarnerthelpful analysis的实用对立 Stackoverflow Productions™可以轻松识别C扩展,提供...... 实际答案。

休闲分歧

我最不喜欢的Stackoverflow类型的答案是&#34;不要这样做,因为我说&#34;品种。不出所料,abarnert其他有用的分析开始于这样一个家长式的熨平板:

  

我认为这没什么用处。

可靠地区分C扩展和非C扩展的能力非常有用,没有它,Python社区就会变得贫穷。真实世界的用例包括:

  • 应用程序冻结,将一个跨平台的Python代码库转换为多个特定于平台的可执行文件。 PyInstaller是此处的标准示例。识别C扩展对于强大的冻结至关重要。如果被冻结的代码库导入的模块是C扩展,则由该C扩展传递链接的所有外部共享库也必须与该代码库一起冻结。 可耻的忏悔:我contribute到PyInstaller。
  • 应用程序优化,或者以及时方式动态地对本机机器代码(例如Cython进行动态调整(例如,{{ 3}})。出于不言自明的原因,Python优化器必须将已编译的C扩展与未编译的纯Python模块区分开来。
  • 依赖关系分析,代表最终用户检查外部共享库。在Numba中,我们分析了一个强制依赖(our case),以检测此依赖关系的本地安装,链接到非并行化的共享库(例如,Numpy),并在此时通知最终用户案子。为什么?因为我们的应用程序由于我们无法控制的依赖关系的不正确安装而表现不佳,所以我们不想要责备。 糟糕的表现是你的错,不幸的用户!
  • 可能是其他重要的低级别内容。分析,也许?

我们都同意冻结,优化和最小化最终用户投诉是有用的。因此,确定C扩展名很有用。

分歧深化

我也不同意reference BLAS implementation的倒数第二个结论:

  

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

不。任何人为此提出的最好的启发式方法是下面给出的。所有stdlib模块(包括 not 仅限于inspect)都无法用于此目的。具体做法是:

  • inspect.getsource()inspect.getsourcefile()函数模糊地返回None两个C扩展(可以理解地没有纯Python源)和其他类型的模块也没有纯Python源(例如,仅字节码模块)。 无用
  • importlib仅机械适用于abarnert可加载的模块,因此默认importlib导入算法可见。 有用,但几乎不适用。当现实世界反复击中你的包裹时,PEP 302合规性的假设就会破裂。例如,您是否知道__import__()内置实际上是PEP 302-compliant loaders 这就是我们用来定制Python导入机制的方法 - 当地球仍然平坦时。

overriddable&#39; abarnert也有争议:

  

......没有完美的答案。

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

让我们找到它。

寻求完美

仅当传递的先前导入的模块对象是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

如果它看起来很长,那是因为文档字符串,注释和断言是好的。它实际上只有六行。 吃掉你老人的心脏,Guido。

布丁证明

让我们用四个可移植的模块测试这个功能:

  • stdlib pure-Python os.__init__模块。 希望不是C扩展。
  • stdlib pure-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的加载器加载(常见案例),则Triforce需要在导入此模块时分配的属性来定义特殊的{{ 1}}属性,其值是加载此模块的加载器对象。因此:
    1. 如果此模块的此值是特定于CPython的__loader__类的实例,则此模块是C扩展名。
  2. 否则,(A)活跃的Python解释器官方CPython实现(例如,PEP 302 specification)或(B)< / strong>活动的Python解释器是CPython,但是这个模块由符合PEP 302的加载器加载,通常是由于默认的importlib.machinery.ExtensionFileLoader机制被覆盖(例如,低 - level bootloader将此Python应用程序作为特定于平台的冻结二进制文件运行。在任何一种情况下,都要回退测试此模块的文件类型是否是特定于当前平台的C扩展名。
  3. 八行功能,有二十页解释。 我们是如何滚动的。

答案 1 :(得分:11)

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

对于一些流行的第三方示例:numpy是纯Python,即使重要的一切都是用C实现的; bintrees是纯Python,即使它的类都可以用C或Python实现,具体取决于你如何构建它;等

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


但如果你想要这样做,你实际上需要区分三个事物:

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

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

由于多种原因,您无法完全根据__file__进行区分:

  • 内置模块根本没有__file__。 (这在一些地方有记录 - 例如inspect文档中的Types and members表。)请注意,如果您使用py2appcx_freeze之类的内容,那么计为“内置”可能与独立安装不同。
  • 纯Python模块可能有一个.pyc / .pyo文件,而在分布式应用程序中没有.py文件。
  • 作为单个文件鸡蛋安装的软件包中的模块(与easy_install相同,pip较少)将有空白或无用的__file__
  • 如果你构建了一个二进制发行版,你的整个库很有可能被打包成一个zip文件,导致与单文件蛋一样的问题。

在3.1+中,导入过程已经大量清理,大部分都是用Python重写的,并且主要暴露在Python层中。

因此,您可以使用importlib模块查看用于加载模块的加载器链,最终您将转到BuiltinImporter(内置),ExtensionFileLoader(。所以/ .pyd / etc。),SourceFileLoader(。py)或SourcelessFileLoader(。pyc / .pyo)。

您还可以在当前目标平台上看到分配给四个中每个的后缀,作为importlib.machinery中的常量。所以,你可以检查any(pathname.endswith(suffix) for suffix in importlib.machinery.EXTENSION_SUFFIXES)),但实际上并没有帮助,例如鸡蛋/拉链盒,除非你已经走完了链条。


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

最佳选择是getsourcegetsourcefilegetfile中的一个或多个;哪个最好取决于你想要的启发式方法。

内置模块会为其中任何一个引发TypeError

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

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

最好的办法是在您关心的分发/设置中试验您关心的平台上的版本。

答案 2 :(得分:0)

虽然Cecil Curry's回答有效(而且信息非常丰富,我可能会补充),即使它包含子模块,它也会为模块的“顶级”返回False使用C扩展名(例如numpy vs. 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)

答案 3 :(得分:0)

@Cecil Curry的功能非常出色。有两个小注释:首先,for (int i = 0; i < dataGridView1.Rows.Count; i++) { for (int a = 0; a < dataGridView2.Rows.Count; a++) { if (dataGridView1.Rows[i].Cells[0].Value == dataGridView2.Rows[i].Cells[0].Value) { dataGridView1.Rows[i].Cells[2].Value = dataGridView2.Rows[a].Cells[2].Value.ToString(); dataGridView1.Rows[i].Cells[3].Value = dataGridView2.Rows[a].Cells[3].Value.ToString(); i++; a++; } } } 示例使用我的Python 3.5.6副本引发了_elementtree。其次,正如@crld所指出的那样,了解模块是否包含 C扩展也很有帮助,但是更具移植性的版本可能会有所帮助。因此,更通用的版本可能是:

TypeError

答案 4 :(得分: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?")