当您导入一个包时会发生什么?

19
为了提高效率,我正在尝试弄清楚Python如何处理其对象堆(以及名称空间系统,但基本上已经清楚)。因此,我基本上正在尝试理解对象何时加载到堆中,有多少个对象存在,它们的寿命是多长等等。
我的问题是:当我使用一个包并从中导入东西时,发生了什么?
from pypackage import pymodule

什么对象会被加载到内存中(Python解释器的对象堆中)?更一般地说:会发生什么?:)

我猜上面的例子做了这样的事情: pypackage包的某个对象在内存中被创建(其中包含有关包但不太多的信息),模块pymodule被加载到内存中,并在本地名称空间中创建了它的引用。重要的是:除非明确声明(在模块本身中或在包初始化技巧和钩子的某个地方,我对此不熟悉),否则不会在内存中创建pypackage的其他模块(或其他对象)。最后,在内存中唯一的大型内容是pymodule(即导入模块时创建的所有对象)。是这样吗?如果有人能澄清这个问题,我会很感激。也许你能提供一些有用的文章?(文档涵盖了更具体的内容)

我在同一个问题的答案中找到了以下内容:

当Python导入一个模块时,首先检查模块注册表(sys.modules)是否已经导入该模块。如果是这样,Python将使用现有的模块对象。

否则,Python会执行以下操作:

  • 创建一个新的空模块对象(这本质上是一个字典)
  • 将该模块对象插入sys.modules字典中
  • 加载模块代码对象(如果必要,首先编译模块)
  • 在新模块的名称空间中执行模块代码对象。代码分配的所有变量都将通过模块对象可用。

并且会很感激对包进行相同类型的解释。

顺便说一句,在包中,模块名称被奇怪地添加到sys.modules中:

>>> import sys
>>> from pypacket import pymodule
>>> "pymodule" in sys.modules.keys()
False
>>> "pypacket" in sys.modules.keys()
True

还有一个关于同样事情的实际问题。

当我构建一组工具时,这些工具可能用于不同的进程和程序,并将它们放在模块中。即使我只想使用其中一个声明的函数,我也别无选择,只能加载完整的模块。我认为可以通过制作小型模块并将它们放入软件包中(如果软件包在您导入其中一个模块时不会加载其所有模块),以减轻此问题的痛苦。

有没有更好的方法在Python中创建这样的库呢?(仅使用纯函数,没有模块内部的任何依赖项)。 这在C扩展中是否可能?

对于这样一个长问题,抱歉。

2个回答

13

您在这里提出了几个不同的问题...

关于导入包

当您导入一个包时,步骤的顺序与导入模块时相同。唯一的区别是,包的代码(即创建“模块代码对象”的代码)是包的__init__.py文件中的代码。

因此,除非__init__.py显式地这样做,否则不会加载包的子模块。如果您执行from package import module,那么只有module被加载,除非它从包中导入其他模块。

sys.modules中从包中加载的模块的名称

当您从包中导入一个模块时,添加到sys.modules中的名称是“限定名称”,它指定了模块名称以及您从中导入它的任何包的点分隔名称。因此,如果您执行from package.subpackage import mod,则添加到sys.modules的内容是"package.subpackage.mod"

仅导入模块的一部分

通常情况下,不必只导入整个模块而不是只有一个函数,您说它很“麻烦”,但实际上几乎从不会这样做。

如果像您所说的那样,函数没有外部依赖关系,则它们只是纯Python,并且加载它们的时间不会太长。通常,如果导入模块需要很长时间,那么就是因为它加载了其他模块,这意味着它确实具有外部依赖性,您必须加载整个模块。

如果您的模块在模块导入时执行昂贵的操作(即全局模块级别代码而不是在函数内部),但并非所有函数使用这些操作都是必需的,则可以重新设计您的模块以推迟到稍后再加载它们。也就是说,如果您的模块执行以下操作:

def simpleFunction():
    pass

# open files, read huge amounts of data, do slow stuff here

你可以将它改为

def simpleFunction():
    pass

def loadData():
    # open files, read huge amounts of data, do slow stuff here
然后告诉别人,“当你想要加载数据时,调用someModule.loadData()。”或者,正如你建议的那样,您可以将模块中昂贵的部分放入其自己的独立模块中,在一个包内。
我从未发现导入模块会导致有意义的性能影响,除非该模块已经足够大,可以合理地分解为较小的模块。制作许多每个仅包含一个函数的小模块不太可能为您带来任何好处,除了因必须跟踪所有这些文件而导致维护困难。您是否实际上有一个特定的情况,这对您有所区别?
此外,关于您最后的观点,据我所知,针对C扩展模块和纯Python模块同样适用全有或全无的加载策略。显然,就像Python模块一样,您可以将事物拆分成更小的扩展模块,但是您不能执行from someExtensionModule import someFunction而不运行打包为该扩展模块一部分的其他代码。

谢谢!非常好的答案。我现在没有很多函数的具体例子,只是为了未来而问。是的,一个函数-一个模块会是一场灾难)合理大小的模块应该能够很好地工作。 - xealits

3
当导入一个模块时,大致的步骤如下:
  1. Python会在sys.modules中查找模块,如果找到就不做任何处理。包的键名是完整的名称,因此虽然pymodule不在sys.modules中,但pypacket.pymodule会在那里(可以使用sys.modules["pypacket.pymodule"]获得)。

  2. Python会找到实现该模块的文件。如果模块是包的一部分,由x.y语法确定,它会查找名为x的目录,其中包含一个__init__.pyy.py(或更深层次的子包)。找到的最底层的文件将是.py文件、.pyc文件或.so/.pyd文件。如果找不到适合该模块的文件,则会引发ImportError

  3. 创建一个空的模块对象,并使用该模块的__dict__作为执行命名空间执行该模块中的代码。1

  4. 将模块对象放置在sys.modules中,并注入到导入器的命名空间中。

第3步是“将对象加载到内存中”的时刻:所涉及的对象是模块对象,以及包含在其__dict__中的命名空间的内容。这个字典通常包含作为执行所有defclass和其他顶级语句时产生的顶级函数和类的副作用的模块中所包含的。

请注意,上述仅描述了import的默认实现。有许多方法可以自定义导入行为,例如通过覆盖内置的__import__或通过实现导入钩子


1 如果模块文件是一个 .py 的源代码文件,它将首先被编译到内存中,然后执行编译产生的代码对象。如果它是一个 .pyc 文件,则通过 反序列化文件内容 获取代码对象。如果模块是一个 .so.pyd 共享库,它将使用操作系统的共享库加载工具加载,并调用 init<module> C 函数来初始化该模块。


1
这个问题特别涉及到*包(package)*而不是独立的模块。你基本上只是重申了提问者已经知道的内容。 - BrenBarn
@BrenBarn OP提出了几个问题,我们选择回答不同的问题。由于他明确提出,“我试图理解对象何时加载到堆中,有多少个对象,它们存在多长时间等等[...]哪些对象被加载到内存中(Python解释器的对象堆中)?”我并不确定他是否知道关于模块的所有这些事情,因为如果他知道,他也会知道所有这些适用于包。我的第二个答案包含特定于导入包的信息,而第一个答案回答了明确的问题,即为什么“pymodule”不在“sys.modules”中。 - user4815162342
非常感谢。即使问题是关于包的,对模块的清晰度也只有好处。而且,既然这些信息现在在StackOverflow上了,对于人们来说它更容易被搜索到。他们不需要从整个互联网收集信息来获取这个简单的结构,就像我们中的一些人所做的那样。实际上,当你在文档、博客、问答等各种地方搜索答案时,你可能已经知道了模块,但不知道包的工作方式相同 ;) - xealits

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