>>> from env_helper import info; info()
页面更新时间: 2023-12-27 09:24:36
运行环境:
    Linux发行版本: Debian GNU/Linux 12 (bookworm)
    操作系统内核: Linux-6.1.0-16-amd64-x86_64-with-glibc2.36
    Python版本: 3.11.2

3.1. 有节制地使用 from...import 语句

Python提供了 3种方式来引人外部模块:import语句、from...import...__import__函 数。 其中较为常见的为前面两种,而 __import__ 函数与import语句类似,不同点在于前者显式地将模块的名称作为字符串传递并赋值给命名空间的变量。

在使用import的时候注意以下几点:

  • 一般情况下尽量优先使用 import a 形式,如访问 B 时需要使用 a.B 的形式。

  • 有节制地使用 from a import B 形式,可以直接访问

  • 尽量避免使用 from a import * ,因为这会污染命名空间,并且无法清晰地表示导入了哪些对象。

为什么在使用import的时候要注意以上几点呢?在回答这个问题之前先来简单了解一下 Python的import机制。 Python在初始化运行环境的时候会预先加载一批内建模块到内存中, 这些模块相关的信息被存放在sys.modules中。 读者导入sys模块后在Python解释器中输入 sys.modules.items()便可显示所有预加载模块的相关信息。 当加载一个模块的时候,解释器实际上要完成以下动作:

  1. sys.modudles中进行搜索看看该模块是否已经存在,如果存在,则将其导人到当前局部命名空间,加载结束。

  2. 如果在sys.modudles中找不到对应模块的名称,则为需要导人的模块创建一个宇典对象,并将该对象信息插入sys.modudles中。

  3. 加载前确认是否需要对模块对应的文件进行编译,如果需要则先进行编译。

  4. 执行动态加载,在当前模块的命名空间中执行编译后的字节码,并将其中所有的对象放人模块对应的宇典中。

我们以用户自定义的模块为例来看看sys.modudles和当前局部命名空间发生的变化。在 Python 的安装目录下创逮一个简单的模块testmodule.py:

>>> a=1
>>> b = 'a'
>>> print( "testing module import")
testing module import

我们知道用户模块未加载之前.sys.modules中并不存在相关信息。那么逬行import testmodule操作会发生什么情况呢?

>>> dir()
['In',
 'Out',
 '_',
 '__',
 '___',
 '__builtin__',
 '__builtins__',
 '__doc__',
 '__loader__',
 '__name__',
 '__package__',
 '__spec__',
 '_dh',
 '_i',
 '_i1',
 '_i2',
 '_ih',
 '_ii',
 '_iii',
 '_oh',
 '_sh',
 'a',
 'b',
 'exit',
 'get_ipython',
 'quit']
>>> import testmodule
>>>
>>> dir()#①import* testmodule之后局部命名空间发生变化
testing module import
['In',
 'Out',
 '_',
 '__',
 '___',
 '__builtin__',
 '__builtins__',
 '__doc__',
 '__loader__',
 '__name__',
 '__package__',
 '__spec__',
 '_dh',
 '_i',
 '_i1',
 '_ih',
 '_ii',
 '_iii',
 '_oh',
 'exit',
 'get_ipython',
 'quit',
 'testmodule']
>>> import sys
>>> 'testmodule' in sys.modules.keys()
True
>>> id(testmodule)
140098020245568
>>> id(sys.modules['testmodule'])
140098020245568
>>> dir(testmodule)
['__builtins__',
 '__cached__',
 '__doc__',
 '__file__',
 '__loader__',
 '__name__',
 '__package__',
 '__spec__',
 'a',
 'b']
>>> sys.modules['testmodule' ].__dict__.keys()
dict_keys(['__name__', '__doc__', '__package__', '__loader__', '__spec__', '__file__', '__cached__', '__builtins__', 'a', 'b'])

从输出结果可以看出,对于用户定义的模块,import机制会创建一个新的module将其加入当前的局部命名空间中,与此同时,sys.modules也加入了该模块的相关信息。但从它们的id输出结果可以看出,本质上是引用同一个对象。同时会发现testmodule.py所在的目录下多了一个.pyc的文件,该文件为解释器生成的模块相对应的字节码,从import之后的输出 “testing module import”可以看出模块同时被执行,而a和b被写入testmodule所对应的字典信息中。

需要注意的是,直接使用import和使用from a import B形式这两者之间存在一定的差异,后者直接将B暴露于当前局部空间,而将a加载到sys.modules集合。

>>> import sys
>>>
>>> dir ()
['In',
 'Out',
 '_',
 '__',
 '___',
 '__builtin__',
 '__builtins__',
 '__doc__',
 '__loader__',
 '__name__',
 '__package__',
 '__spec__',
 '_dh',
 '_i',
 '_i1',
 '_i2',
 '_i3',
 '_i4',
 '_i5',
 '_ih',
 '_ii',
 '_iii',
 '_oh',
 '_sh',
 'exit',
 'get_ipython',
 'quit',
 'sys']
>>> from testmodule import a
>>> dir()#使用from...import...之后命名空f司发生的变化
['In',
 'Out',
 '_',
 '_1',
 '_10',
 '_11',
 '_12',
 '_15',
 '_16',
 '_3',
 '_5',
 '_8',
 '_9',
 '__',
 '___',
 '__builtin__',
 '__builtins__',
 '__doc__',
 '__loader__',
 '__name__',
 '__package__',
 '__spec__',
 '_dh',
 '_i',
 '_i1',
 '_i10',
 '_i11',
 '_i12',
 '_i13',
 '_i14',
 '_i15',
 '_i16',
 '_i17',
 '_i18',
 '_i19',
 '_i2',
 '_i20',
 '_i21',
 '_i3',
 '_i4',
 '_i5',
 '_i6',
 '_i7',
 '_i8',
 '_i9',
 '_ih',
 '_ii',
 '_iii',
 '_oh',
 'a',
 'exit',
 'get_ipython',
 'quit',
 'sys',
 'testmodule']
>>> sys.modules['testmodule']
<module 'testmodule' from '/home/juper/jubook/pt03_thinking/ch03_basic-improvement/testmodule.py'>
>>> id (sys.modules['testmodule'])
140098020245568

了解完import机制,我们冉来看看对于from a import …无节制的使用会带来什么问题。

3.1.1. 命名空间的冲突

来看一个例于。假设有如下3个文件:a.py,b.py及importtest.py,其中a和b都定义了 add()函数,当在import test文件中同时采用from…import..,的形式导人add的时候,import test中起作用的到底是哪一个函数呢?

文件a.py如下:

>>> class a:
>>>     def add():
>>>         print("add in module A")

文件b.py如下:

>>> class b:
>>>     def add():
>>>         print ("math in module B")

文件 importtest.py 如下:

from a import add from b import add if __name__ == '__main__':

math()

从程序的输出“add in module B”可以看出实际起作用的是最近导人的add(),它完全覆 盖了当前命名空间之前从a中导人的add()。在项目中,特别是大型项目中频繁地使用frotna import …的形式会增加命名空间冲突的概率从而导致出现无法预料的问题。因此需要有节制 地使用from…import语句。一般来说在非常明确不会造成命名冲突的前提下.以下几种情况 下可以考虑使用from…import语句:

  1. 当只需要导入部分属性或方法时。

  2. 模块中的这些属性和方法访问频率较高导致使用“模块名.名称”的形式进行访问过于烦琐时。

  3. 模块的文朽明确说明需要使用from…import形式,导人的是一个包下面的子模块, 且使用from…import形式能够更为简单和便利时a如使用firom io.drivers import zip要比使用 import io.drivers.zip 更方便。

3.1.2. 循环嵌套导入的问题

先来看下面的例子:

cl.py: from c2 import q def x() :

Pass

c2.py: from c1 import x def g():

Pass

无论运行上面哪一个文件都会抛出ImportError异常。这是因为在执行c1.py的加载过 程中,需要创建新的模块对象c1然后执行c1.py所对应的字节码。此时遇到语句from c2 import g,而c2在sys.modules也不存在,故此时创建与c2对应的模块对象并执行 c2.py 所对应的字节码。当遇到c2中的语句from c1 import x时,,由于c1已经存在,于是便去其对应的字典中査找g,但c1模块对象虽然创建但初始化的过程并未完成,因此其对应的字典中并不存在g对象,此时便抛出ImportError: cannot import name g异常。而解决循环嵌套导入问题的一个方法是直接使用import语句。读者可以自行验证。