python导入机制及importlib模块

写在篇前

这篇博客的雏形,严格来讲,在我脑海中浮现已有近一年之久,起源于我之前在写一个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关键字是导入模块最常见的方式,该语句主要包括两个操作:

  1. 搜索给定模块,通过函数__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命名的模块。

  2. 将搜索结果绑定到一个局部作用域(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:

  1. Built-in,可通过dir(__builtins__)查看;
  2. Global,全局命名空间是指包含在主程序层定义的任何对象。Python在主程序启动时创建全局命名空间,并一直存在,直到解释器终止。严格地说,这可能不是唯一存在的全局命名空间。python解释器还为程序用import语句加载的任何模块创建全局命名空间;
  3. 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', 
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值