Как в Python узнать, исходит ли модуль от расширения C?

Каков правильный или наиболее надежный способ определить из Python, если импортированный модуль получен из расширения C в отличие от чистого модуля Python? Это полезно, например, если в пакете Python есть модуль как с чистой реализацией Python, так и с реализацией C, и вы хотите иметь возможность определить во время выполнения, какой из них используется.

Одна из идей состоит в том, чтобы проверить расширение файла module.__file__, но я не уверен, что все расширения файлов следует проверять, и если этот подход обязательно является наиболее надежным.


person cjerdonek    schedule 02.12.2013    source источник
comment
c модули отображаются как встроенные. Используйте dir/проверьте документы, чтобы узнать больше.   -  person Marcin    schedule 03.12.2013
comment
Многие модули на самом деле представляют собой тонкую оболочку Python вокруг реализации C, если она доступна, или вокруг чистого Python, если нет. Так работает большая часть stdlib версии 3.2+, а также многие популярные сторонние модули. Итак, я подозреваю, что это на самом деле не скажет вам, чего вы хотите. Например, numpy — это чистый модуль Python, а pickle — это чистый Python, вне зависимости от того, получены ли _Pickle и его друзья из ускорителя C или из чистого Python.   -  person abarnert    schedule 03.12.2013
comment
@Marcin: «показать как встроенный» где? Если вы просто посмотрите на repr, скажем, cPickle в 2.7, у него есть путь, а не строка built-in. И единственная официальная эвристика для различения встроенных модулей заключается в том, что отсутствует __file__, что опять-таки неверно для cPickle.   -  person abarnert    schedule 03.12.2013
comment
@abarnert Я согласен рассматривать только случай сторонних модулей, в которых не используется промежуточная чистая оболочка Python. По крайней мере, в одном известном мне случае атрибут __file__ модуля расширения заканчивается на .so, когда используется реализация C, но я не знаю, всегда ли это так или обычно.   -  person cjerdonek    schedule 03.12.2013
comment
@cjerdonek: Это определенно не всегда так. Смотрите мой ответ для полной информации.   -  person abarnert    schedule 03.12.2013


Ответы (5)


Во-первых, я не думаю, что это вообще полезно. Модули очень часто являются оболочками на чистом Python для модуля расширения C или, в некоторых случаях, оболочками на чистом Python для модуля расширения C, если он доступен, или реализацией на чистом Python, если нет.

Некоторые популярные сторонние примеры: numpy — это чистый Python, хотя все важное реализовано на C; bintrees — это чистый Python, хотя все его классы могут быть реализованы либо на C, либо на Python, в зависимости от того, как вы его строите; и т.п.

И это верно для большинства стандартных библиотек, начиная с 3.2. Например, если вы просто import pickle, классы реализации будут построены на C (то, что вы использовали, чтобы получить от cpickle в 2.7) в CPython, в то время как они будут чисто версиями Python в PyPy, но в любом случае pickle сам по себе является чистым Python .


Но если вы делаете это, вам нужно различать три вещи:

  • Встроенные модули, такие как sys.
  • Модули расширения C, такие как cpickle 2.x.
  • Чистые модули Python, такие как pickle 2.x.

И это при условии, что вы заботитесь только о 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, чтобы увидеть цепочку загрузчиков, используемых для загрузки module, и в конечном итоге вы получите BuiltinImporter (встроенные модули), ExtensionFileLoader (.so/.pyd/и т. д.), SourceFileLoader (.py) или SourcelessFileLoader (.pyc/.pyo).

Вы также можете увидеть суффиксы, назначенные каждому из четырех на текущей целевой платформе, как константы в importlib.machinery. Таким образом, вы можете проверить, что any(pathname.endswith(suffix) for suffix in importlib.machinery.EXTENSION_SUFFIXES)), но на самом деле это не поможет, например, в случае с яйцом/молнией, если только вы уже не продвинулись вверх по цепочке.


Лучшие эвристики, которые кто-либо придумал для этого, — это те, которые реализованы в модуле inspect, поэтому лучше всего использовать их.

Лучшим выбором будет один или несколько из getsource, getsourcefile и getfile; что лучше всего зависит от того, какую эвристику вы хотите.

Встроенный модуль поднимет TypeError для любого из них.

Модуль расширения должен возвращать пустую строку для getsourcefile. Кажется, это работает во всех версиях 2.5-3.4, которые у меня есть, но у меня нет 2.4. Для getsource, по крайней мере, в некоторых версиях, он возвращает фактические байты файла .so, даже если он должен возвращать пустую строку или поднимать IOError. (В 3.x вы почти наверняка получите UnicodeError или SyntaxError, но вы, вероятно, не хотите полагаться на это…)

Модули Pure Python могут возвращать пустую строку для getsourcefile, если в файле egg/zip/etc. Они всегда должны возвращать непустую строку для getsource, если исходный код доступен, даже внутри яйца/zip/и т. д., но если они представляют собой байт-код без исходного кода (.pyc/и т. д.), они вернут пустую строку или вызовут IOError .

Лучше всего поэкспериментировать с интересующей вас версией на интересующей вас платформе(ах) в дистрибутивах/настройках, которые вам интересны.

person abarnert    schedule 02.12.2013
comment
Даже inspect.getsource не работает должным образом. Для двоичного модуля он возвращает двоичное строковое содержимое ELF, для Pure py он возвращает исходный код. Тест на RedHat EL5, python 2.4.3. И EL6, питон 2.6.6. Это слишком старо? - person PasteBT; 03.12.2013
comment
@PasteBT: Это действительно не должно возвращать содержимое строки ELF… позвольте мне просмотреть историю, чтобы увидеть, изменилось ли это в какой-то момент. Может быть, вы хотите использовать getsourcefile и/или getsourcelines напрямую? - person abarnert; 03.12.2013
comment
Fedora 17 + python 2.7.3 работает. Двоичный модуль вызывает исключение. И getsourcefile лучше соответствует вашему описанию. Даже для старой версии - person PasteBT; 03.12.2013
comment
@PasteBT: я воспроизвел двоичные модули, возвращающие двоичный исходный код в 2.5 и 3.1, хотя в 3.1 вы получаете UnicodeError или SyntaxError, пытаясь обработать заголовок Mach-O/ELF/PE как текст. Однако, по крайней мере, в версиях 2.7 и 3.4 getsource иногда работает для заархивированных исходных файлов, а getsourcefile — нет. Итак, у них обоих есть преимущества и недостатки, и нет идеального ответа. - person abarnert; 03.12.2013
comment
Есть есть идеальный ответ, но ни модули inspect, ни importlib stdlib не подходят. Почему? Поскольку значения, возвращаемые inspect.getsource() и inspect.getsourcefile(), не различают расширения C (которые не имеют исходного кода на чистом Python) и другие типы модулей, также не имеющий исходного кода на чистом Python (например, модули, содержащие только байт-код). Аналогично, механизм importlib применяется только к модулям, загружаемым с помощью загрузчиков, совместимых с PEP 302, что не обязательно. Тем не менее, этот ответ отлично подходит для исследования неочевидных нюансов. - person Cecil Curry; 02.09.2016
comment
Вот сценарий, в котором это более чем полезно, это необходимо: функции AWS Lambda. В Lambda предустановлены некоторые библиотеки. Контейнер времени выполнения исчезает, поэтому нет возможности установить больше. Вы можете добавить библиотеки в пакет Lambda и добавить эти каталоги в sys.path во время выполнения. Странно, но работает хорошо. Изюминка заключается в библиотеках для конкретных платформ. Чистые библиотеки можно скопировать из любой ОС, в которой вы разрабатываете, но библиотеки для конкретной ОС необходимо создать в другом экземпляре Amazon Linux и скопировать в папки вашего поставщика. Это PITA, поэтому вы хотите делать это только для библиотек, отличных от Python. - person Chris Johnson; 17.10.2016

тл;др

Проверенный ответ см. в подразделе «В поисках совершенства» ниже.

В качестве прагматичного противовеса 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) бесполезны для этой цели. Конкретно:

  • Функции inspect.getsource() и inspect.getsourcefile() неоднозначно возвращают None как для расширений C (которые по понятным причинам не имеют исходного кода на чистом Python), так и для других типов модулей, которые также не имеют исходного кода на чистом Python (например, модули только для байт-кода). Бесполезно.
  • importlib механизм только применяется к модулям, загружаемым загрузчиками, совместимыми с PEP 302 и, следовательно, виден алгоритму импорта importlib по умолчанию. Полезно, но едва ли применимо в целом. Предположение о соответствии PEP 302 рушится, когда реальный мир неоднократно сталкивается с вашим пакетом. Например, знаете ли вы, что встроенный __import__() на самом деле переопределяемый? Вот как мы настраивали механизм импорта Python, когда Земля была еще плоской.

abarnert окончательный вывод также является спорным:

… нет идеального ответа.

Есть идеальный ответ. Подобно часто сомнительному Triforce из легенды Хайрула, на каждый несовершенный вопрос существует идеальный ответ.

Давайте найдем это.

В поисках совершенства

Следующая функция на чистом Python возвращает True только в том случае, если переданный ранее импортированный объект модуля является расширением C: Для простоты предполагается 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 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. If the passed module was loaded by a PEP 302-compliant loader (the common case), the PEP 302 specification requires the attribute assigned on importation to this module to define a special __loader__ attribute whose value is the loader object loading this module. Hence:
    1. If this value for this module is an instance of the CPython-specific importlib.machinery.ExtensionFileLoader class, this module is a C extension.
  2. В противном случае либо (A) активный интерпретатор Python не является официальной реализацией CPython (например, PyPy) или (B) активным интерпретатором Python является CPython, но этот модуль не был загружен загрузчиком, совместимым с PEP 302, обычно из-за механизм по умолчанию __import__() переопределяется (например, низкоуровневым загрузчиком, запускающим это приложение Python как замороженный двоичный файл для конкретной платформы). В любом случае вернитесь к проверке, является ли тип файла этого модуля типом расширения C, специфичным для текущей платформы.

Восемь функций в строке с пояснениями на двадцати страницах. Вот как мы работаем.

person Cecil Curry    schedule 03.09.2016

Функция @Cecil Curry превосходна. Два небольших комментария: во-первых, пример _elementtree вызывает TypeError с моей копией Python 3.5.6. Во-вторых, как указывает @crld, также полезно знать, содержит ли модуль расширения C, но может помочь более переносимая версия. Таким образом, более общие версии (с синтаксисом f-строки Python 3.6+) могут быть:

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()
person Rudolf Cardinal    schedule 22.09.2018
comment
Фрагмент не работает, так как 1) отсутствует объект log (исправьте, добавив, например, import logging ; log = logging.getLogger(__name__)) и 2) отсутствует функция is_module_a_package() (я не знаю, как ее реализовать) - person akaihola; 21.04.2020
comment
Спасибо - отредактировано соответственно; пример теперь самодостаточен и протестирован под Python 3.6. - person Rudolf Cardinal; 28.04.2020

Хотя ответ Сесила Карри работает (и был очень информативным, как и abarnert'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)
person crld    schedule 01.10.2017

Если вы, как и я, увидели отличный ответ @Cecil Curry и подумали, как я могу сделать это для всего файла требований суперленивым способом без сложного обхода дочерней библиотеки @Rudolf Cardinal, не смотрите дальше!

Во-первых, выгрузите все ваши установленные требования (при условии, что вы сделали это в виртуальной среде и здесь нет других вещей) в файл с pip freeze > requirements.txt.

Затем запустите следующий сценарий, чтобы проверить каждое из этих требований.

Примечание: это очень лениво и НЕ БУДЕТ работать для многих библиотек, чьи имена импорта не совпадают с именами их пипсов.

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?")

person theannouncer    schedule 26.01.2021