[英]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 社區將變得貧困。 實際用例包括:
我們都同意凍結、優化和盡量減少最終用戶的投訴是有用的。 因此,識別 C 擴展很有用。
我也不同意abarnert的倒數第二個結論:
任何人為此提出的最好的啟發式方法是在
inspect
模塊中實現的方法,所以最好的辦法就是使用它。
不。任何人為此提出的最好的啟發式方法是下面給出的那些。 所有STDLIB模塊(包括但不限於inspect
)是無用用於這一目的。 具體來說:
inspect.getsource()
和inspect.getsourcefile()
函數為 C 擴展(可以理解沒有純 Python 源)和其他類型的模塊也沒有純 Python 源(例如,僅字節碼的模塊inspect.getsourcefile()
返回None
)。 沒用。importlib
機制僅適用於可通過符合 PEP 302 的加載程序加載的模塊,因此對默認的importlib
導入算法可見。 有用,但幾乎不普遍適用。 當現實世界反復沖擊您的包裹時,PEP 302 合規性假設就失效了。 例如,您是否知道__import__()
內置函數實際上是可覆蓋的? 這就是我們用來自定義 Python 的導入機制的方式——回到地球還是平的時候。……沒有完美的答案。
有一個完美的答案。 就像經常被懷疑的海拉魯傳說中的三角力量一樣,每個不完美的問題都有一個完美的答案。
讓我們找到它。
僅當傳遞的先前導入的模塊對象是 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
如果它看起來很長,那是因為文檔字符串、注釋和斷言都很好。 它實際上只有六行。 把你年邁的心吃掉,圭多。
讓我們用四個可移植的可導入模塊對這個函數進行單元測試:
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
,則實現類將在 CPython cpickle
C 構建(您曾經在 2.7 中從cpickle
獲得),而它們將在 PyPy 中是純 Python 版本,但無論哪種方式, pickle
本身都是純的蟒蛇。
但如果你真的想這樣做,你實際上需要區分三件事:
sys
。cpickle
。pickle
。並且假設您只關心 CPython; 如果您的代碼在 Jython 或 IronPython 中運行,則實現可能是 JVM 或 .NET 而不是本機代碼。
由於多種原因,您無法根據__file__
完美區分:
__file__
。 (這在幾個地方都有記錄——例如, inspect
文檔中的類型和成員表。)請注意,如果您使用的是py2app
或cx_freeze
類的東西,那么算作“內置”的可能與獨立安裝不同.easy_install
中很常見,在pip
不太常見)將有一個空白或無用的__file__
。在 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
模塊中實現的方法,所以最好的辦法就是使用它。
最佳選擇是getsource
、 getsourcefile
和getfile
一個或多個; 哪個最好取決於您想要哪種啟發式方法。
內置模塊將為它們中的任何一個TypeError
。
擴展模塊應該為getsourcefile
返回一個空字符串。 這似乎適用於我擁有的所有 2.5-3.4 版本,但我沒有 2.4。 對於getsource
,至少在某些版本中,它返回 .so 文件的實際字節,即使它應該返回空字符串或IOError
。 (在 3.x 中,您幾乎肯定會得到UnicodeError
或SyntaxError
,但您可能不想依賴它……)
如果在 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.