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

cje*_*nek 8 python python-c-extension extension-modules

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

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

Cec*_*rry 16

TL;博士

有关经过充分测试的答案,请参阅下面的"寻找完美"小节.

作为abarnert 可扩展识别C扩展所涉及的微妙性的有用分析的实用对照,Stackoverflow Productions™提供了...... 一个实际的答案.

休闲分歧

我最不喜欢的Stackoverflow类型的答案是"不要这样做,因为我说"变种.不出所料,abarnert的其他有用的分析开始于这样一个家长式的同伴:

我认为这根本没用.

能够可靠地区分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()功能不明确地返回None两个C扩展(其可以理解没有纯Python源)和其它类型的模块,也没有纯Python源(例如,仅字节代码模块)的.没用.
  • importlib机器适用于可由符合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
Run Code Online (Sandbox Code Playgroud)

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

布丁的证明

让我们用四个可移植的可导入模块对这个功能进行单元测试:

  • stdlib纯Python os.__init__模块.希望不是C扩展.
  • stdlib纯Python importlib.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
Run Code Online (Sandbox Code Playgroud)

一切都好了,结束了.

你怎么做的

我们的代码的细节是非常无关紧要的.很好,我们从哪里开始?

  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扩展名.

八行功能,二十页解释.我们是如何滚动的.


aba*_*ert 11

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

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

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


但是,如果你希望这样做,你其实需要区分3件事情:

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

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

__file__由于以下原因,您无法完美区分:

  • 内置模块根本没有__file__.(这是记录在几个地方,例如,类型和成员表中inspect的文档.)请注意,如果你使用像py2app或者cx_freeze,什么算是"内置"可能是从独立安装不同.
  • 纯Python模块可能具有.pyc/.pyo文件,而在分布式应用程序中没有.py文件.
  • 安装为单文件鸡蛋的软件包中的模块(常见的easy_install,较少使用pip)将具有空白或无用__file__.
  • 如果你构建一个二进制发行版,你的整个库很有可能被打包成一个zip文件,导致与单文件蛋相同的问题.

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

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

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


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

最好的选择将是一个或多个getsource,getsourcefilegetfile; 哪个最好取决于你想要的启发式方法.

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

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

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

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