# 模块与包

  • 模块(Module)

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

    • 在一个目录中加入一个名为 __init__.py 的文件(内容可以为空),Python 解释器就会将该目录识别为包。
    • 使用包,便于管理同一目录下的多个模块。
    • 支持在一个包目录中嵌套另一个包目录。
  • 库(Library)

    • 可以将能独立实现某些功能的 Python 代码封装成一个库,发布给其他用户使用。
    • 简单的库可能只包含一个模块,复杂的库可能包含几十个包。

# import

  • 在脚本中,用 import 关键字可以导入某个模块或包:

    import sys                  # 导入 sys 模块
    print('path :', sys.path)  # 调用 sys 模块的成员
    
    • import xx 语句导入一个包时,相当于执行 import xx.__init__
  • from xx import xx 语句可以导入某个模块或包的成员,加入当前作用域:

    from sys import argv, path
    print('path :', path)      # 不需要用 sys. 开头,就可以直接访问导入的成员。不过这样可能与当前作用域的标识符重名
    
    from sys import *           # 用 * 可以导入全部成员。不过这样可读性差,如果不需要导入很多成员,则不建议这样做
    
  • as 关键字可以将导入的标识符重命名:

    import sys as _sys
    from sys import path as _path
    

# 寻址

  • 导入某个模块或包时,Python 解释器会在 sys 模块的 path 目录列表中寻找该文件并导入。

    • 如果没找到则报错: ModuleNotFoundError: No module named 'xx'
    • 用户可以主动在 sys.path 中增加搜索路径,不过每次重启 Python 解释器时都会重置 sys.path 的值。
  • 用 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 语句,则执行该语句块时才会执行 import 语句,并且导入的模块只能在该局部作用域内被访问。
  • pyinstaller 在打包 Python 脚本时会收集局部导入的模块,即使该 import 语句不会被执行。

# 间接导入

  • 导入一个模块时,实际上是先执行它的全部内容(像正常执行脚本一样),然后将它创建的所有对象保留到该模块的作用域中,供当前脚本访问。

    • 例如:如果用 Python 解释器执行 1.py ,而 1.py 导入了 2.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
      
  • 在包内的脚本中,可以用相对地址进行导入:

    from .utils import *    # 从当前目录导入(需要当前目录是一个包)
    from .. import *        # 从上层目录导入(需要当前目录、上层目录都是包)
    
    • 如果该脚本被间接执行,则 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)
      
      

# 脚本的内置属性

  • __file__ :脚本的文件路径,由 Python 解释器自动赋值。

    • 当脚本被直接运行时,其值为执行该脚本时使用的文件路径(可能是绝对路径、相对路径);当脚本被间接运行时,其值为该脚本的绝对路径。
  • __name__ :脚本的名称,由 Python 解释器自动赋值。

    • 当脚本被直接运行时,其值为 "__main__" ;当脚本被间接运行时,其值为该脚本的模块名,即将脚本的文件名去掉 .py 后缀。
    • 例如:下方语句块只会在脚本被直接运行时执行,当脚本被间接运行时不会执行。
      if __name__ == '__main__':
          try:
              main()
          except:
              raise
      
    • 例如,编写一个 test.py :
      print(__file__)
      print(__name__)
      
    • 在终端执行该脚本时,其输出为:
      [root@CentOS ~]# python test.py
      test.py
      __main__
      
      [root@CentOS ~]# python /root/test.py
      /root/test.py
      __main__
      
    • 在 Python 解释器中导入该脚本时,其输出为:
      >>> import test
      /root/test.py
      test
      
  • __all__ :一个列表,决定了用 from xx import * 导入该脚本时会导入哪些成员。

    • 如果用户没有定义该属性,则默认会导入该脚本内的所有公有成员(即标识符不以下划线 _ 开头)。
    • 例:
      from sys import path
      
      __all__ = ['path', 'fun1']
      
      def fun1():
          pass