python进阶——模块化(二)模块和包

(三)模块(Module)

1. 概念:

Python中的模块,是一个Python的文件,以".py"为后缀,其余部分作为模块名称,包括Python的对象定义和Python的一组可执行代码语句(函数和类)。

一个模块对应了一组特定的功能,一些定义的对象或者变量使用。

模块是按照逻辑组织Python代码的方法,文件是按照物理组织模块内Python代码的方法——因此,一个文件被看作是一个独立模块,一个模块可以被看作是一个文件,模块文件名=模块名+.py。

模块的名称实际上就存储在全局变量__name__中,以字符串表示。

可以通过导入sys库调用其modules功能来获得__name__的对应模块名。

1
2
3
4
import sys
print(sys.modules[__name__])
# 运行结果:
# <module '__main__' from 'D:\\pycharmwork\\blog_use.py'>

2. 模块的创建:

Python中一般使用三个方法创建模块:

  • 创建一个python文件,并用python语言编写其功能。
  • 用C语言实现功能,在运行时动态加载。
  • 直接引用内置模块。

绝大多数时候我们只使用第一个方法,自己写一个.py为后缀的文件即可。

编写一个模块主要是要注意不要直接编写可执行代码,而是把这些代码封装在函数中或者类中,再定义好需要对外使用的变量即可。

另外要注意.py文件的对应模块名必须符合标识符的命名规则。

3. 模块的导入和调用:

(1)import:

import语句可以引入已经定义好的模块。

  • 导入整个模块:

    对于导入整个模块,import语句的语法格式为:import module1[, module2[,... moduleN]],moduleN表示一个模块名。

    此时,可以调用模块的所有功能,调用的格式为:模块名.函数名,模块名是不能省略的。

    例如导入Python的math库并使用其sqrt函数:

    1
    2
    3
    4
    5
    import math
    result = math.sqrt(4)
    print(result)
    # 运行结果:
    # 2.0

    要注意,一个模块只会被导入一次,以此来防止导入模块被一遍遍执行。

    1
    2
    3
    4
    5
    6
    7
    8
    #如在pycharm中编写会发现只有第三个import是高亮的
    import math
    import math
    import math
    result = math.sqrt(4)
    print(result)
    # 运行结果:
    # 2.0

    如果想要重复导入执行模块顶层部分的代码,使用reload()函数重新导入。

    语法格式:reload(module_name)

    不过奇葩的是,reload函数的调用需要导入iimportlib包才能使用。

    1
    2
    3
    4
    5
    6
    7
    8
    import time
    import importlib
    print(time.struct_time)
    importlib.reload(time)
    print(time.struct_time)
    # 运行结果:
    # <class 'time.struct_time'>
    # <class 'time.struct_time'>

    可以使用dir()函数返回模块内定义的所有模块、变量和函数。

    语法格式:dir(module),返回一个列表,列表即为上述的全部定义信息。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    import math
    result = dir(math)
    j=0
    for i in result:
    print(i,end=" ")
    j+=1
    if j== 10:
    print()
    j=0
    # 运行结果:
    # __doc__ __loader__ __name__ __package__ __spec__ acos acosh asin asinh atan
    # atan2 atanh cbrt ceil comb copysign cos cosh degrees dist
    # e erf erfc exp exp2 expm1 fabs factorial floor fmod
    # frexp fsum gamma gcd hypot inf isclose isfinite isinf isnan
    # isqrt lcm ldexp lgamma log log10 log1p log2 modf nan
    # nextafter perm pi pow prod radians remainder sin sinh sqrt
    # tan tanh tau trunc ulp

    这其中有三个固定方法:

    • __doc__:指向指定对象的注释部分。
    • __file__:指向该模块的导入文件名。
    • __name__:指向模块的名字。
  • 导入模块中的特定函数和变量:

    当只需要一个模块的某些函数和变量时,可以只导入模块的指定函数和变量到当前的命名空间中。

    使用from ...import语句,语法格式:from modname import name1[, name2[, ... nameN]]

    modname是模块名,nameN是函数名或者变量名。

    此语句在全局命名空间中只会引入指定的变量和函数名称。

    和import语句不同的是,import语句不会将这些符号保存到全局命名空间中,所以其调用必须显式指定模块名,而from....import语句是保存到全局命名空间中,所以不需要在调用时指定模块名,可以当做自己的函数使用。

    注意导入功能名字有重复时,调用的是最后定义或者导入的功能。

    例如导入math中的sin()、cos()和tan()函数以及pi变量:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    from math import sin,cos,tan,pi
    print(globals().keys())
    print(sin(pi/2))
    print(cos(pi))
    print(tan(pi/4))
    # 运行结果:
    # dict_keys(['__name__', '__doc__', '__package__', '__loader__', '__spec__', '__annotations__', '__builtins__', '__file__', '__cached__', 'sys', 'sin', 'cos', 'tan', 'pi'])
    # 1.0
    # -1.0
    # 0.9999999999999999
  • 导入模块中的全部内容:

    除了指定内容,也可以全部的模块内容导入到当前的命名空间中。

    仍使用from...import语句,但import内容为*,代表导入模块的全部内容。

    语法格式为:from modname import *,modname为模块名。

    注意这样的操作可能会导致全局命名空间有大量的冗余变量和函数,可能使其内存占用大大增加,产生很多命名冲突,和我们使用模块化的初衷不符,所以不建议过多使用。

    另外,该方式不会导入下划线__开头的函数。

    例如将time模块全部导入,能够看到time中的全部函数、类和变量都被保存到了全局命名空间中。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    from time import *
    j=0
    for i in list(globals().keys()):
    print(i,end=" ")
    j+=1
    if j==10:
    print()
    j=0
    print("\n-----------------------------")
    j=0
    for i in list(dir(time)):
    print(i,end=" ")
    j+=1
    if j==10:
    print()
    j=0
    # 运行结果:
    # __name__ __doc__ __package__ __loader__ __spec__ __annotations__ __builtins__ __file__ __cached__ sys
    # time time_ns sleep gmtime localtime asctime ctime mktime strftime strptime
    # monotonic monotonic_ns process_time process_time_ns thread_time thread_time_ns perf_counter perf_counter_ns get_clock_info timezone
    # altzone daylight tzname struct_time j
    # -----------------------------
    # __call__ __class__ __delattr__ __dir__ __doc__ __eq__ __format__ __ge__ __getattribute__ __getstate__
    # __gt__ __hash__ __init__ __init_subclass__ __le__ __lt__ __module__ __name__ __ne__ __new__
    # __qualname__ __reduce__ __reduce_ex__ __repr__ __self__ __setattr__ __sizeof__ __str__ __subclasshook__ __text_signature__
    • __all__变量:

      如果模块文件中定义了__all__变量,使用from modname import *语句时,只能导入这个列表的元素。

      另外,该变量允许导入以__为开头的变量,所以请谨慎使用。

      下面展示一下导入时的情况

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      #test.py文件代码
      __all__ = ["fun2","fun3"]
      def fun():
      print("执行模块功能中...")
      print("执行完成")
      def fun2():
      print("功能2")
      print("2完成")
      def fun3():
      print("功能3")
      print("3完成")

      #执行文件
      from test import *
      if "fun" in globals().keys():
      print("True fun")
      if "fun2" in globals().keys():
      print("True fun2")
      if "fun3" in globals().keys():
      print("True fun3")
      # 运行结果:
      # True fun2
      # True fun3
  • 为导入模块定义别名:

    如果觉得原来的模块名比较繁琐,可以为模块名增加一个别名,简化代码的调用写法。

    格式:import modname as newname,modname是原模块名,newname是新的别名。

    在调用时使用两个中的任何一个均可。

    例如为math模块增加一个ma别名:

    1
    2
    3
    4
    import math as ma
    print(ma.pi)
    # 运行结果:
    # 3.141592653589793
  • 模块的单独运行:

    有时我们希望运行或者测试单个python文件时运行代码执行某些操作或者输出结果,但是作为模块被其他文件引用时不运行这些代码,也就是让这个python文件既能够作为脚本单独运行,又能够作为模块被其他文件使用。

    此时需要为python文件中所有单独运行的代码上增加这样一句话:

    **if __name__ == "__main__":**

    此语句的意思是,只有当前文件单独执行时才会执行此语句下的代码块,作为模块被引用时不会被执行。

    例如我们在一个文件夹下创建.py文件并执行文件,命名为test.py,文件内容和运行结果如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    def fun():
    print("执行模块功能中...")
    print("执行完成")
    if __name__ == "__main__":
    print("测试模块功能中")
    fun()
    print("无问题")
    # 运行结果:
    # 测试模块功能中
    # 执行模块功能中...
    # 执行完成
    # 无问题

    然后在另外一个py文件中导入此模块:

    1
    2
    3
    4
    5
    import test
    test.fun()
    # 运行结果:
    # 执行模块功能中...
    # 执行完成

    可以发现此时的if __name__ == "__main__":语句下的代码块未被执行。

4. 模块的搜索路径:

当导入一个模块时,Python解释器需要对模块进行路径搜索,以找到最适配的模块文件。

模块的基本搜索顺序如下:

  • 首先搜索内置模块,一旦查找到内置模块符合要求立刻导入内置模块——内置模块名与自定义的模块名同名时会导致自定义的模块名被覆盖,其功能无法使用,所以不能自定义和内置模块名相同的模块。

  • 内置模块不匹配,查找当前执行文件所在目录下的模块,如果匹配则导入,不匹配则继续下一步——如果是一些简单的自定义模块,建议放在同一个文件夹下(上面的例子就是如此)。

  • 如果不在当前目录,则搜索在shell变量PYTHONPATH(python环境变量)下的每个目录。

    PYTHONPATH是一个列表,内部的元素是许多目录,这些目录会存储一些python模块文件。

    PYTHONPATH允许我们修改和添加目录,具体步骤此处不再讲述,可自行查阅。

  • 如果PYTHONPATH仍未找到,则查找默认路径。默认路径对于不同的安装方式、安装版本和操作系统来说是不同的,不是统一的。

    对于UNIX,默认路径一般为/usr/local/lib/python

  • 如果上述查找均未能找到匹配的模块名,查找失败,模块不可用。

模块的搜索路径存储在system模块的sys.path变量中,可以通过输出此列表来查看全部的上述搜索路径。

1
2
3
4
5
6
7
8
9
10
11
from sys import path
i=0
for j in path:
print(j,end=" \t")
i+=1
if i==5:
print()
i=0
# 运行结果:
# D:\pycharmwork D:\pycharmwork D:\python\python311.zip D:\python\DLLs D:\python\Lib
# D:\python D:\pycharmwork\venv D:\pycharmwork\venv\Lib\site-packages D:\python\Lib\site-packages

(四)包(Package)

1. 概念:

包是模块的集合,可以理解为是模块的容器,通过包-模块的方式构建了命名的空间,避免了多模块之间的课件的命名冲突。

以文件系统类比时,包实际上就是一个分层次的文件目录结构,它定义了一个由模块及子包,和子包下的模块和子包等组成的 Python 的应用环境——包是文件系统中的目录,模块是目录中的文件

2. __init__.py文件:

对于Python3.3之前,__init__.py文件是每个包和子包必须具备的文件,用于标识该目录是一个包。即使在Python3.3之后,为了版本的兼容以及包的结构的完整,一般情况下也会在每个包或者子包上包含和配置该文件。

__init__.py文件允许是一个空文件,但一般会在此文件中执行包的初始化代码或者设置上面的__all__变量。

一般情况下,新建包时通常会自动创建__init__.py文件__来控制包的导入,如果没有请记得手动创建。

3. 包的导入:

(1)只导入包:

import 包名,注意只导入包名相当于只执行最外层包的__init__.py文件

(2)导入包中的模块:

语法格式和上面的模块import的格式类似,但是模块名需要转变为包名.[子包名].模块名

另外,对包内的模块使用from 包名.[子包名].模块名 import *时,一般要求对应的__init__.py文件设置__all__变量(此处不设置了,包内函数非常简单)。

导入模块时,相当于执行路径上每一个包的__init__.py文件和模块文件本身。

下面简单举例。

假设有一个包名为test_2的包,包和执行文件在同一级的同一目录内,其有两个子模块mod1、mod2和一个子包test_2_inter,子包内有一个模块mod_inter,所有的__init__.py文件为空,模块的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#test_2/mod1.py
def mod1_fun1():
print("mod1_fun1")
def mod1_fun2():
print("mod1_fun2")
mod1_val =10

#test_2/mod2.py
def mod2_fun1():
print("mod2_fun1")
def mod2_fun2():
print("mod2_fun2")
mod2_val =20

#test_2/test_2_inter/mod_inter.py
def mod_inter_fun1():
print("mod_inter_fun1")
def mod_inter_fun2():
print("mod_inter_fun2")
mod_inter_val=30

将每个模块分别导入,执行对应函数,有:

1
2
3
4
5
6
7
8
9
10
import test_2.mod1
from test_2.mod2 import *
import test_2.test_2_inter.mod_inter
test_2.mod1.mod1_fun1()
mod2_fun2()
test_2.test_2_inter.mod_inter.mod_inter_fun1()
# 运行结果:
# mod1_fun1
# mod2_fun2
# mod_inter_fun1

如果__init__内有内容,执行结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#test_2/__init__.py
print("导入test_2包 ing")

#test_2/test2_2_inter/__init__.py
print("导入test_2包 ing")

#执行文件
import test_2.mod1
from test_2.mod2 import *
import test_2.test_2_inter.mod_inter
test_2.mod1.mod1_fun1()
mod2_fun2()
test_2.test_2_inter.mod_inter.mod_inter_fun1()
# 运行结果:
# 导入test_2包ing
# 导入完成
# 导入test_2包的test2_2_inter子包ing
# 导入完成
# mod1_fun1
# mod2_fun2
# mod_inter_fun1

注意,__init__文件只执行一次。

如果想要一次性全部导入,需要使用__all__变量和from 包名 import *

1
2
3
4
5
6
7
8
9
10
11
12
13
from test_2 import *
from test_2.test_2_inter import *
mod1.mod1_fun1()
mod2.mod2_fun2()
mod_inter.mod_inter_fun1()
# 运行结果:
# 导入test_2包ing
# 导入完成
# 导入test_2包的test2_2_inter子包ing
# 导入完成
# mod1_fun1
# mod2_fun2
# mod_inter_fun1

不增加__all__变量无法使用from 包名 import *格式,必须使用包名.模块名格式。

(2)导入包中的子模块的函数或变量:

和上面的逻辑类似,只是导入的方式改为函数或者变量。

使用from 包名.[子包名].模块名 import 函数/变量 格式来完成。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
from test_2.mod1 import mod1_fun1
from test_2.mod2 import mod2_val
from test_2.test_2_inter.mod_inter import mod_inter_fun2
mod1_fun1()
print(mod2_val)
mod_inter_fun2()
# 运行结果:
# 导入test_2包ing
# 导入完成
# 导入test_2包的test2_2_inter子包ing
# 导入完成
# mod1_fun1
# 20
# mod_inter_fun2
(3)相对路径导入包和模块:

在查找使用包内的某个模块或者模块内的函数/类/变量时,需要使用绝对导入指定绝对路径或者使用相对导入指定相对路径,从而在包查找到匹配的模块。

注意,这两种方式都是针对包内的模块而言的,而执行文件查找包名时,使用的是前面模块的查询顺序。也就是说,先通过内置模块->当前目录->环境变量目录->默认目录查找到包名,再通过相对或者绝对路径查找到指定的包内模块。这两者不要混为一谈。

  • 绝对导入:

    是默认的导入方式,因为它更常见,并且它有相对导入的所有功能。

    绝对导入的出发目录是项目的根目录,需要写明从根目录到要导入模块的完整路径——项目的根目录就是绝对路径。

    绝对路径要求必须从最顶层的目录开始为每个包或者模块提供完整详细的导入路径。

    绝对导入的缺点是:包名一旦改变,导入的语句也需要发生改变;导入的路径可能十分冗长复杂。

    绝对导入就是我们上面所使用的方式,这里不再介绍,这里只讲述相对导入和相对路径

  • 相对导入:

    首先要明确,相对导入只能是包内的模块使用的,用于导入包内的其他模块,且只能作为模块文件使用相对导入,外部的执行文件是不会使用相对导入的。

    相对导入的出发目录是当前目录,需要给出相对路径,从当前目录出发查找模块。

    相对路径是指待导入模块与当前执行文件的相对位置,用一个"."来代替当前文件的所在目录。

    from后有一个"."时,"."表示可以访问同级目录下的包或者模块;有两个"."时,可以访问上一级目录的包或者模块。

    相对导入的优势是代码中不会出现包的名称,不需要修改代码,同时多数情况下能够简化路径的长度。

    例如,在上面例子中的子模块test_2/test_2_inter中的mod_inter.py文件内,使用相对路径导入test_2/mod1模块并使用其函数,再由外部函数调用:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    #mod_inter.py
    from ..mod1 import mod1_fun1
    def mod_inter_fun1():
    mod1_fun1()
    print("mod_inter_fun1")
    def mod_inter_fun2():
    print("mod_inter_fun2")
    mod_inter_val=30

    #执行文件:
    from test_2.test_2_inter.mod_inter import mod_inter_fun1
    mod_inter_fun1()
    # 运行结果:
    # 导入test_2包ing
    # 导入完成
    # 导入test_2包的test2_2_inter子包ing
    # 导入完成
    # mod1_fun1
    # mod_inter_fun1

5. import的运行和查看导入模块:

执行import命令时,首先检查导入模块是否在已有模块中,有的话此import命令无效,直接跳过。

这能够防止模块之间相互引用的无限循环。

随后,在sys.modules中搜索名为os的模块,如果其缓存存在,将缓存映射内容返回;不存在,继续搜索内置模块,os是预先安装的,所以一般情况下此时就能查找到对应结果;如果依旧没有,就在sys.path列表定义的路径中继续搜索。

搜索成功后,在本地作用域和命名空间内初始化相应的模块对象。

可以使用sys内置模块查看已导入的模块,可以得到sys.modules对象,该对象是一个字典,键是模块名,值是文件路径。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
from test_2.test_2_inter.mod_inter import mod_inter_fun1
import sys
j=0
for i in sys.modules:
print(i,end="\t")
j+=1
if j==5:
print()
j=0
#运行结果:
# 导入test_2包ing
# 导入完成
# 导入test_2包的test2_2_inter子包ing
# 导入完成
# sys builtins _frozen_importlib _imp _thread
# _warnings _weakref winreg _io marshal
# nt _frozen_importlib_external time zipimport _codecs
# codecs encodings.aliases encodings encodings.utf_8 _signal
# _abc abc io __main__ _stat
# stat _collections_abc genericpath _winapi ntpath
# os.path os _sitebuiltins _codecs_cn _multibytecodec
# encodings.gbk itertools keyword _operator operator
# reprlib _collections collections types _functools
# functools importlib._bootstrap importlib._bootstrap_external warnings importlib
# importlib.machinery importlib._abc posixpath enum _sre
# re._constants re._parser re._casefix re._compiler copyreg
# re fnmatch errno urllib urllib.parse
# pathlib zlib _compression _bz2 bz2
# _lzma lzma shutil math _bisect
# bisect _random _sha512 random _weakrefset
# weakref tempfile contextlib collections.abc _typing
# typing.io typing.re typing importlib.resources.abc importlib.resources._adapters
# importlib.resources._common importlib.resources._legacy importlib.resources importlib.abc importlib.util
# _virtualenv _distutils_hack mpl_toolkits site test_2
# test_2.test_2_inter test_2.mod1 test_2.test_2_inter.mod_inter

python进阶——模块化(二)模块和包
http://example.com/2023/07/15/python进阶——模块化(二)模块和包/
作者
07xiaohei
发布于
2023年7月15日
许可协议