如果导入的模块来自C 扩展而不是纯 Python 模块,那么从 Python 中判断的正确或最可靠的方法是什么?这很有用,例如,如果一个 Python 包包含一个具有纯 Python 实现和 C 实现的模块,并且您希望能够在运行时判断正在使用哪个模块。
一个想法是检查 的文件扩展名module.__file__
,但我不确定应该检查的所有文件扩展名,以及这种方法是否一定是最可靠的。
如果导入的模块来自C 扩展而不是纯 Python 模块,那么从 Python 中判断的正确或最可靠的方法是什么?这很有用,例如,如果一个 Python 包包含一个具有纯 Python 实现和 C 实现的模块,并且您希望能够在运行时判断正在使用哪个模块。
一个想法是检查 的文件扩展名module.__file__
,但我不确定应该检查的所有文件扩展名,以及这种方法是否一定是最可靠的。
tl;博士
有关经过充分测试的答案,请参阅下面的“寻找完美”小节。
作为与abarnert对可移植识别 C 扩展所涉及的微妙之处的有益分析的务实对比,Stack Overflow Productions™ 提出了……一个实际的答案。
可靠地区分 C 扩展和非 C 扩展的能力非常有用,没有它,Python 社区将变得一贫如洗。实际用例包括:
我们都同意冻结、优化和最小化最终用户投诉是有用的。因此,识别 C 扩展很有用。
我也不同意abarnert的倒数第二个结论:
任何人为此想出的最好的启发式方法都是在
inspect
模块中实现的,所以最好的办法就是使用它。
不。任何人为此想出的最佳启发式方法如下所示。所有的 stdlib 模块(包括但不限于inspect
)都不能用于此目的。具体来说:
inspect.getsource()
andinspect.getsourcefile()
函数会模棱两可地返回。没用。None
importlib
machine仅适用于可由符合 PEP 302 的加载器加载的模块,因此对默认importlib
导入算法可见。有用,但很难普遍适用。当现实世界反复撞击您的包裹时,PEP 302 合规性的假设就会失效。例如,您知道__import__()
内置实际上是可覆盖的吗?这就是我们用来自定义 Python 导入机制的方式——回到地球还平坦的时候。……没有完美的答案。
有一个完美的答案。就像经常被怀疑的 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
如果它看起来很长,那是因为文档字符串、注释和断言都很好。实际上只有六行。吃掉你年老的心脏,圭多。
让我们用四个可移植的可导入模块对这个函数进行单元测试:
os.__init__
模块。希望不是 C 扩展。importlib.machinery
子模块。希望不是 C 扩展。_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
一切都结束了。
我们代码的细节是无关紧要的。很好,我们从哪里开始?
__loader__
属性,其值是加载此模块的加载程序对象。因此:
importlib.machinery.ExtensionFileLoader
类的实例,则此模块是 C 扩展。__import__()
机器被覆盖(例如,通过低级引导加载程序将此 Python 应用程序作为特定于平台的冻结二进制文件运行)。在任何一种情况下,都回退到测试该模块的文件类型是否是特定于当前平台的 C 扩展的文件类型。八行功能,二十页解释。这就是我们滚动的方式。
首先,我认为这根本没有用。模块是围绕 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
.cpickle
.pickle
.这是假设你只关心 CPython;如果您的代码在 Jython 或 IronPython 中运行,则实现可能是 JVM 或 .NET,而不是本机代码。
您无法根据 完美区分__file__
,原因有很多:
__file__
。(这在几个地方都有记录——例如,文档中的类型和成员表inspect
。)请注意,如果您使用类似py2app
or的东西cx_freeze
,那么算作“内置”的内容可能与独立安装不同。easy_install
与.pip
__file__
在 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
模块中实现的,所以最好的办法就是使用它。
最佳选择将是 、 和 中的一个getsource
或getsourcefile
多个getfile
;哪个最好取决于您想要哪种启发式方法。
内置模块将为其中TypeError
任何一个提出 a 。
扩展模块应该为getsourcefile
. 这似乎适用于我拥有的所有 2.5-3.4 版本,但我没有 2.4。对于getsource
,至少在某些版本中,它返回 .so 文件的实际字节,即使它应该返回一个空字符串或引发一个 .so 文件IOError
。(在 3.x 中,您几乎肯定会得到一个UnicodeError
or SyntaxError
,但您可能不想依赖它……)
getsourcefile
如果在 egg/zip/etc 中,纯 Python 模块可能会返回一个空字符串。如果源可用,即使在 egg/zip/etc. 中,它们也应该始终返回非空字符串getsource
,但如果它们是无源字节码 (.pyc/etc.),它们将返回空字符串或引发 IOError。
最好的选择是在您关心的发行版/设置中在您关心的平台上试验您关心的版本。
@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()
尽管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)
如果您像我一样看到@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?")