# 模块

# import

  • 一个 py 脚本文件,可以被 Python 解释器直接运行,也可以通过 import 语句导入其它脚本文件,此时该脚本文件称为一个模块(module)。

  • 用关键字 import 可以将某个模块,导入到当前脚本。

    • 例:
      >>> import sys      # 导入一个名为 sys 的模块
      >>> sys
      <module 'sys' (built-in)>
      >>> sys.platform    # 调用 sys 模块的成员
      'linux'
      
  • 执行 from xx import xx 语句,可以从某个模块,导入指定名称的成员(即导入某个标识符)。

    • 例:
      >>> from sys import platform
      >>> platform
      'linux'
      
    • 这种语法的优点:
      • platform 这样的标识符,比 sys.platform 更短,写代码时更方便。
    • 这种语法的缺点:
      • platform 这样的标识符,没有前缀,可能与当前作用域的某个标识符重名。
      • 代码的可读性差。当读者看到 platform 这样的标识符时,不知道它是当前脚本定义的变量,还是通过 import 导入的。
    • 可以同时导入多个成员,用逗号分隔:
      >>> from sys import platform, path
      
    • 可以用 import * 导入全部成员。但这样不能看出具体导入了哪些成员,导致代码的可读性很差,建议不要这样做。
      from sys import *
      
  • 可以用关键字 as 将导入的标识符重命名:

    import sys as _sys
    from sys import path as _path
    
  • 一般将 import 语句写在 Python 脚本的开头,使得导入的标识符,可在全局作用域内访问。

    • 也可以将 import 语句写在某个函数或方法内,使得执行该语句块时才会执行 import 语句,并且导入的模块只能在该局部作用域内被访问。
    • pyinstaller 在打包 Python 脚本时,会将局部导入的各个模块一起收集,即使该 import 语句位于不会被执行的 if 分支。

#

  • 在一个文件目录中,如果存在一个名为 __init__.py 的文件(内容可以为空),则 Python 解释器会将该目录识别为一个包(package)。

    • 一个包,主要作用是充当一个命名空间。
      • 一个包,可以包含多个 Python 模块,作为该包的成员。
      • 一个包,可以嵌套包含其它包目录。
    • import 语句可以导入一个包,但本质上是导入包中的一个模块。
      • 执行 import <package> 语句,相当于执行 import <package>.__init__ ,导入包中一个名为 __init__ 的模块。
      • 因此,每个包必须包含一个名为 __init__.py 的文件。
  • 例:假设磁盘中存在以下文件目录

    ├── mypackage
    │   ├── __init__.py
    │   ├── module1.py
    │   └── module2.py
    └── test.py
    

    此时 mypackage 会被识别为一个包,可以在 test.py 文件中,执行以下几种 import 语句:

    import mypackage
    import mypackage.module1
    from   mypackage import module1
    from   mypackage.module1 import xx
    

# 寻址

  • 执行 import xx 可以导入某个模块或包,但 Python 解释器如何知道这个模块或包,的实际文件路径?

    • sys.path 的取值为 list 类型,记录了几个常用的目录。Python 解释器会到这些目录及其子目录中,查找名为 xx 的模块或包。
    • 如果找到了,则导入。如果没找到,则抛出异常: ModuleNotFoundError: No module named 'xx'
    • 例:
      >>> sys.path
      ['', '/usr/lib64/python39.zip', '/usr/lib64/python3.9', ...]
      
    • 用户可以添加其它目录到 sys.path 中:
      >>> sys.path.append('/tmp')
      
      不过每次重启 Python 解释器时,都会重置 sys.path 的值,除非声明终端环境变量 export PYTHONPATH=/tmp
  • 如果一个脚本位于一个包内,则可以通过相对地址导入其它模块或包:

    from .utils import *    # 从当前目录导入(需要当前目录是一个包)
    from .. import *        # 从上层目录导入(需要当前目录、上层目录都是包)
    
    • 如果该脚本被间接执行,则 Python 解释器会自动将这些相对地址,转换成绝对地址。
    • 如果该脚本被直接执行,则 Python 解释器不能处理相对地址,会报错。
  • 如果模块名、包名会变化,如何导入?可以调用 importlib 模块的 import_module() 函数,输入一个 str 字符串作为模块名或包名,然后导入。

    >>> from importlib import import_module
    >>> m = import_module('os')                   # 相当于 import os
    >>> m
    <module 'os' from '/usr/lib64/python3.6/os.py'>
    >>> m = import_module('os.path')              # 相当于 import os.path
    >>> m
    <module 'posixpath' from '/usr/lib64/python3.6/posixpath.py'>
    >>> m = import_module('sys', package='os')    # 相当于 from os import sys
    >>> m
    <module 'sys' (built-in)>
    

# 间接执行

  • 通过 import 导入一个模块时,实际上会执行一遍该模块的全部 Python 代码,然后将其中创建的所有对象,保存到当前脚本的作用域,供当前脚本访问。

    • 例如:用 Python 解释器执行 a.py ,而 a.py 导入了 b.py ,则最终两个脚本都会被执行一次,只不过一个是被直接执行,一个是被间接执行。
    • 例如,编写一个 test.py :
      print('Hello')
      
      def fun1():
          print('fun1')
      
      def fun2():
          print('fun2')
          fun1()
      
    • 然后在终端导入该脚本:
      >>> from test import fun2
      Hello                       # 这里间接执行了 print('Hello')
      >>> fun2()
      fun2
      fun1                        # 导入一个函数或类时,其调用的其它内容会被自动导入
      >>> fun1()                  # 但是并不能被直接访问,因为不存在于当前作用域
      NameError: name 'fun1' is not defined
      >>> from test import fun1   # 重复导入同一个模块,不会重复执行它。因此这里不会重复执行 print('Hello')
      >>> fun1()
      fun1
      
      • 每个模块只会在第一次导入时,执行其中的 Python 代码。因此,如果修改了某个模块的内容,则需要重启 Python 解释器,才能重新导入该模块。
  • Python 解释器导入一个模块时,会在该模块的同一目录下,生成一个 __pycache__ 文件夹,用于缓存该模块预编译生成的字节码文件(扩展名为 .pyc 或.pyo )。

    • 当 Python 解释器下一次导入该模块时,会检查该模块文件是否发生变化,没有的话就读取之前的 __pycache__ ,从而更快地导入。
    • __pycache__ 中会记录每个模块文件的修改日期,从而判断每个模块文件是否发生变化。

# 循环导入

  • 循环导入,是指两个 Python 模块相互导入,或者多个 Python 模块的导入关系形成一个回路。
    • 这种操作是可行的,并不会引发报错。
    • 此时相当于将多个模块中的所有 Python 语句,按导入顺序组合成一个大型脚本,可能产生冲突。
  • 例:
    1. 创建两个循环导入的脚本文件:
      A.py :
      print('start', __name__)
      
      import B
      id = 1
      print(B.id)
      
      print('end', __name__)
      
      B.py :
      print('start', __name__)
      
      import A
      id = 2
      print(A.id)
      
      print('end', __name__)
      
    2. 执行时输出如下:
      [root@CentOS ~]# python3 A.py
      start __main__
      start B
      start A                               # 此时 B.py 导入 A.py ,从头执行其中的代码,重复执行了 print('start', __name__)
      Traceback (most recent call last):
        File "A.py", line 3, in <module>    # 此时 A.py 尚未完成初始化,就导入 B.py ,输出 start B
          import B
        File "B.py", line 3, in <module>    # 此时 B.py 尚未完成初始化,就导入 A.py ,输出 start A
          import A
        File "A.py", line 5, in <module>    # 此时在 A.py 中访问 B.id ,但该变量尚未创建,所以报错
          print(B.id)
      AttributeError: partially initialized module 'B' has no attribute 'id' (most likely due to a circular import)
      
    3. 将 A.py 中的 print(B.id) 改为 print(dir(B)) ,就可以循环导入。如下:
      [root@CentOS ~]# python3 A.py
      start __main__
      start B
      start A
      ['__builtins__', '__cached__', '__doc__', '__file__', '__loader__', '__name__', '__package__', '__spec__']
      end A             # 此时 B.py 尚未完成初始化,但导入的 A.py 已经完整执行了一次,完成了初始化
      1                 # 此时可以在 B.py 中访问 A.id
      end B
      ['A', '__builtins__', '__cached__', '__doc__', '__file__', '__loader__', '__name__', '__package__', '__spec__', 'id']
      end __main__
      
      也可以将 A.py 中的 print(B.id) 放到不会立即执行的代码块中:
      def test():
          print(B.id)