文章目录
写在篇前
这篇博客的雏形,严格来讲,在我脑海中浮现已有近一年之久,起源于我之前在写一个python模块并用jupyter notebook测试时发现,当在一个session
中通过import
导入模块,修改模块再次通过import
导入该模块时,模块修改并不会生效。至此,通过一番研究发现,python 导入机制(import machinery)对于我们理解python这门语言,有着至关重要的作用。因此,本篇博客主要探讨 python import machinery原理及其背后的应用。
import 关键字
关键字import
大家肯定都非常熟悉,我们可以通过import
导入不同功能的模块 (modules)和包 (packages)。在这里,显然import
关键字本身不是我们的重点。因此,我们仅以简略的形式介绍一下python import
语句的使用,后面来重点关注import
语句背后的导入机制及更深层次的用法。import
语句导入主要包括以下形式:
import <module_name>
from <module_name> import <name(s)>
from <module_name> import <name> as <alt_name>
import <module_name> as <alt_name>
import <module_name1>, <module_name2>, <module_name3>, ... # 为了代码规范,不推荐该使用方式
from <module_name> import * # 不推荐
我们举个例子, 假设我们写了一个模块mod.py
,在该模块中定义了一个字符串s
,一个list a
, 一个函数foo
和一个类Foo
:
s = "If Comrade Napoleon says it, it must be right."
a = [100, 200, 300]
def foo(arg):
print(f'arg = {
arg}')
class Foo:
pass
通过import
导入mod.py
模块,可以使用dir()
函数查看导入前后当前命名空间变量的变化:
>>> dir()
['__annotations__', '__builtins__', '__doc__', '__loader__', '__name__', '__package__', '__spec__']
>>> import mod
>>> dir()
['__annotations__', '__builtins__', '__doc__', '__loader__', '__name__', '__package__', '__spec__', 'mod']
>>> mod.__file__
'/Users/jeffery/mod.py'
>>> mod.__name__
'mod'
>>> mod.s
'If Comrade Napoleon says it, it must be right.'
>>> mod.a
[100, 200, 300]
>>> mod.foo(1)
arg = 1
>>> mod.Foo()
<mod.Foo object at 0x10bf421d0>
>>> s
NameError: name 's' is not defined
我们可以发现,import mod
不会使调用者直接访问到mod.py
模中块内容,只是将<module_name>
放在调用者的**命名空间(namespace)**中;而在模块(mode.py
)中定义的对象则保留在模块的命名空间中。因此,从调用者那里,只有通过点表示法 (dot notation) 以<module_name>
作为前缀,才能访问模块中的对象。当然,我们可以通过from <module_name> import *
的方式,将模块中定义的对象导入到当前调用者的命名空间中(以下划线_
开头的对象除外):
>>> from mod import *
>>> dir()
['Foo', '__annotations__', '__builtins__', '__doc__', '__loader__', '__name__', '__package__', '__spec__', 'a', 'foo', 's']
>>> s
'If Comrade Napoleon says it, it must be right.'
import
关键字是导入模块最常见的方式,该语句主要包括两个操作:
-
搜索给定模块,通过函数
__import__(name, globals=None, locals=None, fromlist=(), level=0)
实现;该函数默认被
import
语句调用,可以通过覆盖builtins.__import__
模块来改变导入的语义(不推荐这么做,也不推荐用户使用该函数,建议用户使用importlib.import_module
)。name
要导入的模块;globals
,字典类型,全局变量,用于确定如何解释包上下文中的名称;locals
,忽略该参数;fromlist
,表示应该从name
模块中导入的对象或子模块的名称;level
,表征使用绝对导入或相对导入,0
表示决定导入,>0
表示相对导入,数值指示相对导入的层级;
比如,
import spam
,将会调用spam = __import__('spam', globals(), locals(), [], 0)
;import spam.ham
,则调用spam = __import__('spam.ham', globals(), locals(), [], 0)
;from spam.ham import eggs, sausage as saus
,则调用:_temp = __import__('spam.ham', globals(), locals(), ['eggs', 'sausage'], 0) eggs = _temp.eggs saus = _temp.sausage
因此,当
name
变量的形式为package.module
时,通常将会返回最高层级的包(第一个点号之前的名称),而不是以name
命名的模块。 但是,当给出了fromlist
非空时,则将返回以name
命名的模块。 -
将搜索结果绑定到一个局部作用域(
import
语句来实现)。即__import__()
函数搜索并创建模块,然后其返回值被用来实现import
的name binding操作
导入模块时,python首先会检查模块缓存sys.modules
是否已经导入该模块,若存在则直接返回;否则Python首先会搜索该模块,如果找到该模块,它将创建一个模块对象(types.ModuleType
),对其进行初始化。如果找不到命名的模块,则会引发ModuleNotFoundError
。
import
语句本身的使用就是这么简单,但是本节中提到的几个概念,如package
, module
, namespace
,还需我们有一个更深入的理解。
先导概念
namespace & scope
Namespaces are one honking great idea—let’s do more of those!
— The Zen of Python, by Tim Peters
如大佬所言,命名空间(namespace, a mapping from names to objects) 是非常极其十分伟大的想法。不同的namespace有不同的生命周期,当Python执行程序时,它会根据需要创建namespace,并在不再需要时删除它们。通常,在任何给定的时间都会存在许多命名空间,在python中主要包括三大类的namespace:
- Built-in,可通过
dir(__builtins__)
查看; - Global,全局命名空间是指包含在主程序层定义的任何对象。Python在主程序启动时创建全局命名空间,并一直存在,直到解释器终止。严格地说,这可能不是唯一存在的全局命名空间。python解释器还为程序用import语句加载的任何模块创建全局命名空间;
- Local,函数中定义的局部变量,函数参数等;
我们可以分别通过dir(__builtins__)
or __builtins__.__dict__
, globals()
以及locals()
查看内置、当前全局及局部命名空间中的对象:
>>> def func(x):
... a = 2
... print(locals())
...
>>> func(5)
{
'x': 5, 'a': 2}
>>>
>>> globals()
{
'__name__': '__main__', '__doc__': None, '__package__': None, '__loader__': <class '_frozen_importlib.BuiltinImporter'>, '__spec__': None, '__annotations__': {
}, '__builtins__': <module 'builtins' (built-in)>, 'func': <function func at 0x2b256f36ae50>}
>>> dir(__builtins__)
['ArithmeticError', 'AssertionError', 'AttributeError', 'BaseException', 'BlockingIOError', 'BrokenPipeError', 'BufferError', 'BytesWarning', 'ChildProcessError', 'ConnectionAbortedError', 'ConnectionError', 'ConnectionRefusedError', 'ConnectionResetError', 'DeprecationWarning', 'EOFError', 'Ellipsis', 'EnvironmentError', 'Exception', 'False', 'FileExistsError', 'FileNotFoundError', 'FloatingPointError', 'FutureWarning', 'GeneratorExit', 'IOError', 'ImportError', 'ImportWarning', 'IndentationError', 'IndexError', 'InterruptedError', 'IsADirectoryError', 'KeyError', 'KeyboardInterrupt', 'LookupError', 'MemoryError', 'ModuleNotFoundError', 'NameError', 'None', 'NotADirectoryError', 'NotImplemented', 'NotImplementedError', 'OSError', 'OverflowError', 'PendingDeprecationWarning', 'PermissionError', 'ProcessLookupError', 'RecursionError', 'ReferenceError', 'ResourceWarning', 'RuntimeError', 'RuntimeWarning', 'StopAsyncIteration', 'StopIteration', 'SyntaxError', 'SyntaxWarning', 'SystemError', 'SystemExit', 'TabError', 'TimeoutError', 'True', 'TypeError', 'UnboundLocalError', 'UnicodeDecodeError', 'UnicodeEncodeError', 'UnicodeError', 'UnicodeTranslateError', 'UnicodeWarning', 'UserWarning', 'ValueError', 'Warning', 'ZeroDivisionError', '_', '__build_class__', '__debug__', '__doc__', '__import__', '__loader__', '__name__', '__package__', '__spec__', 'abs', 'all', 'any', 'ascii', 'bin', 'bool', 'breakpoint', 'bytearray',