理解Python的import

Python的import是一个使用非常频繁的操作,这是在一个模块中使用另一个模块的代码的操作(几乎所有的语言都有类似的语句)。import语句是最常用的方法,但不是唯一的方法,还有importlib.import_module() 和 __import__() 等。本文解释import语句。

import做了两件事情:

  1. 搜索该名字的module(其实使用的还是内置的__import__()方法)然后初始化一个对象
  2. 将结果与本地的一个变量名绑定(默认就是这个module的名字)。

意味着,这两句是等价的:import package as pk 和 pg = __import__('package')

值得注意的是,只调用__import__()只会执行搜索动作,初始化module对象,最后丢掉该对象(因为没有赋值操作)。这点很容易将内置的__imoprt__()方法和import语句搞混。

一、Packages

Python中只有一种module类型(type),所有的module,无论是C语言实现的,还是Python实现的,还是别的,都是这种了类型。为了使名字空间有等级,Python又有packages的概念。注意这里只是一个“概念”而并不是类型,packages只是一种特殊的“module”。官方文档中将packages比作文件系统的文件夹,module比作文件(但又不完全是)。所有的package都是module,而并不是所有的module都是package。

Python的package又有两种:

Regular package

Regular package就是一个包含 __init__.py文件的文件夹。当此包导入的时候,会执行这个__init__.py文件,然后就可以包名访问包内的名字了。例如下面的文件结构:

当导入parent.one时会执行parent/__init__.pyparent/one/__init__.py

Namespace package

这是PEP 420提出的。

Namespace package由各种不同的部分(portions)组成。portion可以是zip文件,可以使网络上的资源,可以是文件系统中某个位置的资源。Namespace package在文件系统中可能没有实际的文件来表示,可能是虚拟的module。

# TODO read 420 理解namespace

下面来讲import的搜索顺序。

二、搜索

开始搜索之前,需要一个“合适的名字”(来自import语法或者__import__()函数等),即一个通过点分隔的路径名。比如foo.bar.baz,Python会先导入foo,然后导入foo.bar,然后导入foo.bar.baz,如果中间有任何失败,就会抛出ModuleNotFoundError

缓存(sys.modules)

import首先搜索的地方是sys.modules,此为一个键值对的mapping,作为一个缓存,保存了中间形式的路径。所以如果之前import了 foo.bar.baz,那么sys.modules中就会存在foo, foo.bar, foo.bar.baz。每一个名字都对应一个module对象。

import的时候,如果在sys.modules中找到就会返回这个module对象,import过程结束。如果根据key找到了,但是value是None,就会抛出ModuleNotFoundError,如果key不存在,就继续搜索过程。

sys.modules是可写的,删除任何key并不会销毁对象(因为可能别的模块引用了这个模块),但是你可以将value设置为None强制抛出ModuleNotFoundError。下面的代码展示了这个操作(注意使用ipython结果不同,ipython在启动的时候import 了re库)。

要注意的是,如果你有module对象的引用(在sys.module的缓存),但是重新import了module对象,这两个module不会一致。也就是说你再引用一遍,代码中的该module还是从sys.module中的,所以你拿不到新的module。但是使用importlib.reload() 就可以将改module对象重新初始化一遍。这在REPL中测试代码的时候比较实用。

Finder和Loader

如果在缓存(sys.module)中没有找到的话,python就开始import机制来导入这个包。Python的import协议包括两部分:Finder和Loader。只要实现了这两部分,就算是一个importer,importer如果发现自己可以导入目标包的话,就会返回他们自己。顾名思义,Finder的任务是定位包,并不做载入的部分,Loader做真正载入包的工作。Python有三个默认的importer:一个可以导入build-in module,第二个可以导入frozen module, 第三个可以搜索import path。

三、加载

加载过程的伪代码如下:

注意一下细节:

  • sys.module已经存在目标模块的名字,import直接返回此模块
  • 在Loader执行模块代码之前,先将其放入到sys.module。这一点非常重要,可以防止循环导入(模块可能在代码中导入该模块自己)。
  • 如果loading失败,失败的模块(只有失败的模块)会从sys.module删除
  • 在module创建但是执行代码之前,import会设置 import-related 模块属性,作为后续的总结
  • 模块执行的部分才是模块的名字空间真正暴露的时候,这部分完全由loader处理。
  • loading的时候创建的模块、传入exec_module()的模块可能并不是最后返回的模块

Loader

Loader提供了loading最重要的部分:模块的执行。import机制会调用importlib.abc.Loader.exec_module(),参数只有一个,要执行的module对象。其返回值被丢弃。Loader必须满足两个要求:

  • module必须是一个Python的module对象,Loader必须在module的全局名字空间下(module.__dict__)执行module的代码
  • 如果不能执行代码,必须抛出ImportError,其余所有异常都会被忽略

子模块

当子模块的loading执行的时候,将和父模块的名字空间绑定。参考下例:

导入foo将使spamspam.foo属性:

就先写到这里,其实还有一个比较重要的话题就是模块的搜索顺序,以及子模块之间如何组织。新手尝尝遇到在子模块中遇到不能import其他模块的问题,以后再写吧。

彩蛋

import this可以看到Zen of Python

import antigravity可以看到一幅漫画

参考和了解更多

  1. python文档:https://docs.python.org/3/reference/import.html
  2. import hook和meta path

理解Python的import”已经有5条评论

  1. 归纳的好棒

    “也就是说你再引用一遍,代码中的该module还是从sys.module中的,**所以你拿不到新的module**。”

    这里是不是有点问题, 我看文档上写的是:

    import是重新导入了新的module, reload只是重新初始化了旧的module

    • import不会导入新的module,但是reload确实是会重新处理化旧的module。

      参考下面的代码:

      如果已经import了一个a.py, 然后用编辑器修改了a.py,然后importlib.reload(a) 就可以载入编辑之后的内容。

      https://docs.python.org/3/reference/import.html#the-module-cache

Leave a comment

电子邮件地址不会被公开。 必填项已用*标注