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

23

如何从Python中正确或最可靠地判断一个导入的模块是来自C扩展还是纯Python模块?例如,如果一个Python包有一个既有纯Python实现又有C实现的模块,您希望在运行时能够确定使用哪个。

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


C模块显示为“内置”。使用dir/检查文档以了解更多信息。 - Marcin
许多模块实际上是一个薄的Python包装器,如果可用,则包装C实现,否则包装纯Python。大多数3.2+标准库都是这样工作的,许多最受欢迎的第三方模块也是如此。因此,我怀疑这实际上不会告诉您想要的内容。例如,numpy是一个纯Python模块,而pickle无论_Pickle和朋友们是来自C加速器还是纯Python,都是纯Python。 - abarnert
1
@Marcin:在哪里显示为“builtin”?如果您只查看例如2.7的cPicklerepr,它具有路径名,而不是字符串“built-in”。而区分内置模块的唯一官方启发式方法是缺少__file__,这对于cPickle也不正确。 - abarnert
@abarnert 我只关心第三方模块的情况,而其中没有使用纯 Python 封装器。我知道至少有一种这样的情况,即在使用 C 实现时扩展模块的 __file__ 属性以 .so 结尾,但我不知道是否始终或通常是这种情况。 - cjerdonek
@cjerdonek:这绝对不总是这样。请查看我的答案以获取完整的详细信息。 - abarnert
5个回答

30
tl;dr: 参见下面的“追求完美”子段落,以获取经过充分测试的答案。
作为对 {{link1:abarnert}} 的 {{link2:有用分析}} 实用性反驳,Stack Overflow Productions™ 呈现了……实际答案
可靠地区分C扩展和非C扩展的能力非常有用,否则Python社区会受到损失。现实世界中的使用情况包括:
  • 应用程序冻结:将一个跨平台的 Python 代码库转换为多个特定平台的可执行文件。PyInstaller 是标准的例子。识别 C 扩展对于冻结的稳健性至关重要。如果被冻结的代码库导入的模块是 C 扩展,那么所有与该 C 扩展传递链接的外部共享库都必须与该代码库一起冻结。可耻的忏悔:我为 PyInstaller 做出了贡献
  • 应用程序优化:静态地将其编译成本地机器代码(例如 Cython)或以 JIT 方式动态优化(例如 Numba)。由于明显的原因,Python 优化器必须区分已经编译的 C 扩展和未编译的纯 Python 模块。
  • 依赖性分析:代表最终用户检查外部共享库。在我们的情况下,我们分析一个强制性的依赖项(Numpy),以检测此依赖项的本地安装是否链接到非并行化的共享库(例如参考 BLAS 实现),并在这种情况下通知最终用户。为什么?因为我们不希望由于依赖项的不正确安装而导致我们的应用程序性能不佳,而这些依赖项超出了我们的控制范围。糟糕的性能是你的错,可怜的用户!
  • 可能还有其他必要的低级别内容。例如分析性能?
我们都可以认同,冻结、优化和最小化终端用户的投诉是有用的。因此,识别 C 扩展是有用的。
分歧加深
我也不同意 abarnert 的倒数第二个结论:
“任何人想出来的最佳启发式方法都是在 inspect 模块中实现的,因此最好的方法就是使用它。”
不是这样的。任何人想出来的最佳启发式方法都是下面给出的方法。所有标准库模块(包括但不限于 inspect)对此都是无用的。具体而言:
  • inspect.getsource()inspect.getsourcefile()函数在C扩展(可以理解为没有纯Python源代码)和其他类型的模块(例如仅有字节码的模块)中都含糊地返回None无用的。
  • importlib机制适用于可由PEP 302-compliant loaders加载并因此对默认importlib导入算法可见的模块。 有用,但很难普遍适用。当现实世界反复打击您的软件包时,PEP 302兼容性的假设会崩溃。例如,您是否知道内置的__import__()实际上是可重载的?这是我们在地球还是平的时候定制Python导入机制的方式。
abarnert最终结论也具有争议性:

……没有完美的答案。

有一个完美的答案。就像传说中海拉鲁三力神三角力量一样,每个不完美的问题都有一个完美的答案。
让我们找到它。
寻找完美
下面的纯Python函数仅在传递的先前导入的模块对象是C扩展时返回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 纯 Python 的 os.__init__ 模块。希望不是 C 扩展。
  • stdlib 纯 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 标准的加载器(通常情况)加载的,则 PEP 302 规范 要求在导入该模块时分配的属性定义一个特殊的 __loader__ 属性,其值为加载此模块的加载器对象。因此:
    1. 如果该模块的值是 CPython 特定的 importlib.machinery.ExtensionFileLoader 类的实例,则该模块是 C 扩展。
  2. 否则,要么 (A) 活动的 Python 解释器不是官方的 CPython 实现(例如 PyPy),要么 (B) 活动的 Python 解释器是 CPython,但是该模块没有被符合 PEP 302 标准的加载器加载,通常是由于默认的 __import__() 机制被覆盖(例如,由低级引导程序运行此 Python 应用程序作为特定平台的冻结二进制文件)。在任一情况下,都要回退到测试该模块的文件类型是否为当前平台特定的 C 扩展。
八行代码功能,需要二十页的解释。这就是我们的风格。

16
首先,我认为这并不是很有用。对于模块来说,非常普遍的情况是它们是纯Python包装C扩展模块 - 或者,在某些情况下,如果可用,则是纯Python的包装C扩展模块;否则是纯Python实现。
对于一些流行的第三方模块,例如numpy是纯Python实现的,尽管所有重要的东西都是用C实现的;bintrees是纯Python实现的,尽管其类可能都是根据你如何构建它在C或Python中实现的等等。
在3.2及以上的大多数标准库中也是如此。例如,如果只是导入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表格。)请注意,如果您使用类似于py2app或者cx_freeze之类的工具,那么“内置”的定义可能与独立安装有所不同。
  • 一个纯Python模块可能会在分发应用程序中拥有.pyc/.pyo文件,而没有.py文件。
  • 作为单个文件egg(这在easy_install中很常见,但在pip中不太常见)安装的包中的模块将具有空白或无意义的__file__
  • 如果构建二进制分发版,你的整个库很有可能被打包到一个zip文件中,从而导致单个文件eggs的相同问题。

  • 在3.1+版本中,导入过程已经得到了大量的清理,大部分被重新编写为Python代码,并且大多数暴露给了Python层。

    因此,您可以使用importlib模块来查看用于加载模块的加载器链,最终可以看到BuiltinImporter (builtins)、ExtensionFileLoader (.so/.pyd等)、SourceFileLoader (.py)或SourcelessFileLoader (.pyc/.pyo)。

    您还可以在importlib.machinery中的常量中查看分配给每个四个后缀,在当前目标平台上。因此,您可以 检查any(pathname.endswith(suffix) for suffix in importlib.machinery.EXTENSION_SUFFIXES)),但这在egg/zip情况下并没有实际帮助,除非您已经旅行过整个链。


    目前为止,人们提出的最佳启发式方法是在inspect模块中实现的,因此最好的做法是使用它。其中一个或多个最佳选择是getsourcegetsourcefilegetfile;哪一个是最佳的取决于您想要使用的启发式方法。对于任何一个内置模块,这三个函数之一都会引发TypeError。对于扩展模块,getsourcefile应该返回空字符串。在我拥有的所有2.5-3.4版本中,这似乎都有效,但我没有2.4版本的环境。对于getsource,至少在某些版本中,它返回.so文件的实际字节,即使它应该返回空字符串或引发IOError。(在3.x中,您几乎肯定会得到UnicodeErrorSyntaxError,但您可能不希望依靠此功能……)如果是纯Python模块,则如果在egg/zip/etc中则应该返回空字符串getsourcefile,但如果源码可用,则始终应该为getsource返回非空字符串,即使在egg/zip/etc中也是如此,但如果它们是无源代码的字节码(.pyc/etc。),则将返回空字符串或引发IOError。最好的方法是在您关心的平台上以您关心的分发/设置为基础进行尝试。

    即使 inspect.getsource 不能很好地工作,对于二进制模块,它返回二进制 ELF 字符串内容,对于纯 Py,则返回源代码。在 RedHat EL5 上测试,Python 版本为2.4.3。还有EL6上的Python 2.6.6。这太旧了吗? - PasteBT
    @PasteBT:它真的不应该返回ELF字符串内容...让我浏览一下历史记录,看看是否在某个时候更改了。也许你想直接使用getsourcefile和/或getsourcelines - abarnert
    Fedora 17 + Python 2.7.3可以正常工作。二进制模块会引发异常。而且getsourcefile更符合您的描述,即使是旧版本也是如此。 - PasteBT
    @PasteBT:我在2.5和3.1中复制了返回二进制“源”的二进制模块,尽管在3.1中,当试图将Mach-O/ELF/PE头文件视为文本时,会出现UnicodeErrorSyntaxError。然而,在至少2.7和3.4中,getsource有时适用于无法使用getsourcefile的压缩源文件。因此,它们都有优点和缺点,没有完美的答案。 - abarnert
    这里有一个场景,它不仅仅是有用的,而且是必要的:AWS Lambda函数。Lambda预装了一些库。运行时容器是短暂的,因此没有安装更多库的方法。您可以将"vendor"库加入Lambda程序包,并在运行时将这些目录添加到sys.path中。奇怪但有效。问题出现在平台特定的库上。纯库可以从您正在开发的任何操作系统复制过来,但需要在另一个Amazon Linux实例中构建OS特定的库并将其复制到供应商文件夹中。这很麻烦,因此您只想为非纯Python库执行此操作。 - Chris Johnson
    显示剩余2条评论

    4

    @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://dev59.com/52Ij5IYBdhLWcg3wYUCQ
    
        ``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://dev59.com/0HA75IYBdhLWcg3wT3L8  # 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()
    

    代码片段失败的原因是: 1)缺少“log”对象(通过添加例如“import logging; log = logging.getLogger(name)”来修复), 2)缺少“is_module_a_package()”函数(我不知道如何实现它)。 - akaihola
    2
    感谢--已进行编辑;示例现在是自包含的,并在Python 3.6下进行了测试。 - Rudolf Cardinal

    2
    虽然Cecil Curry的答案可行(而且像abarnert的答案一样非常有见地),但是如果模块包含使用C扩展的子模块(如numpy和numpy.core.multiarray),即使在模块的“顶层”,它也会返回False。
    虽然可能不太健壮,但以下方法适用于我的当前用例:
    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)
    

    1

    如果像我一样看到了 @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?")
    
    

    网页内容由stack overflow 提供, 点击上面的
    可以查看英文原文,
    原文链接