python 历险记(五)— python 中的模块

前言

这次我们继续探险,来搞定 python 中的模块(module)。兵马未动,粮草先行,开工之前先看看基础是否补齐了_

基础

模块的概念你一定不会陌生吧,这是一个非常宽泛的概念,在各行各业都会用到。这里我们涉及的只是软件中的模块概念。说到模块,就得先了解下模块化程序设计的概念。(如果您对模块化程序设计的概念已经烂熟于心,尽可以略过,而直接跳到下一节)

模块化程序设计

模块化程序设计是指进行程序设计时将一个大程序按照功能划分为若干小程序模块,每个小程序模块完成一个确定的功能,并且在这些模块之间建立必要的联系,通过模块的相互协作完成整个功能的程序设计方法。

——百度百科

举个例子,假如我们要造一辆汽车,就可以先将汽车分成四个部件:发动机、底盘、电气设备、车身。每个部件都是一个模块,都会完成自己专属的功能,同时也预留了和其他部件配合的接口。当这几部分建造完成时,就可以组合在一起,从而完成了整个汽车的建造。

类比一下,汽车就是整个程序,而像发动机,底盘等就是程序中各个小的模块。只有当各个模块正常工作,并且暴露的接口和其他模块的接口完全契合,才能组合成一个完整且正常工作的程序。

模块化有哪些好处?

当然,如果不将程序分解成一个个独立的部分,而是整个一大坨,也能够完成所要的功能。那么为什么教科书还有实际使用中都会提倡模块化程序设计?这样做有什么好处呢?

  1. 控制程序设计的复杂度

    不知你看过《代码大全》没有,里面有一句非常著名的格言:软件的首要使命就是管理复杂度。完整的软件功能复杂度是非常高的,如果不使用有效的方法加以管理,很可能会陷入复杂的泥潭中不可自拔。而将程序分解成模块,则会将整体功能的复杂度有效的下分到各个模块中。每个模块只要能够管理好自己的复杂度就可以了。

  2. 提高代码的重用性

    还是以造汽车为例,假设我造了一个很牛的发动机,多款车型都可以使用它。程序设计也一样,如果一个模块能够完成特定的功能,且与父程序耦合度较小,多个程序都可以使用它。

  3. 易于维护和扩展

    小 A 写了一个程序,并将各个部分划分的非常明确,再加以人性化的函数命名和注释。即使有一天小 A 离职了,小 B 要接过来维护以及在此基础上再开发新的功能也不难。

既然模块化就这么多好处, 强大的 python 当然也会吸收这个优秀的设计思想,并且在语言中有所体现,那就是 python 的模块(module)。

什么是 python 中的模块?

先来看一个示例:

  1. 创建 python 文件 a.py,并在文件中定义函数 sum

    def sum(a, b):
        return a + b
    
  2. 创建 python 文件 b.py, 并调用 sum 函数

    from a import sum
    
    print(sum(1, 2)) # 3
    

文件 a.py 就是一个模块(module),b.py就是一个主模块(main module)。

b.py 中有这么一句 from a import sum ,是指将模块 a 中的 sum 函数导入到当前模块中。我们定义的文件名是 a.py ,而模块名就是去掉后缀后得到的 模块 a。那么能不能再多导入几个函数或者导入模块 a 的全部函数呢?当然可以,这个我们后面讲。

调用模块时,通过文件名就可以确定模块的名字,那么在模块(module)内部,能知道自己姓甚名谁吗?还真能。

每个模块都有一个全局变量 __name__ ,它就是模块的名字。上面 a.py 的内容不变,修改下 b.py 的内容。

import a

print(a.__name__)  # a
print(a.sum(1, 2))  # 3

来,一起总结下:

  1. python 模块(module) 是指包含 python 定义(包括 类,函数,变量)和语句的文件(.py做后缀)
  2. 模块名就是模块文件名称去掉.py 后缀
  3. 在模块内部,可以通过全局变量 __name__ 得到模块名称

引入模块有几种方式?

要导入模块并调用,前提要导入的 python 模块中有料(函数,变量,class)才可以。先来定义一个 python 模块 calc

def plus(a, b):
    return a + b


def subtract(a, b):
    return a - b

再创建一个 main.py 文件,在其中做引入操作。okay,准备好了,那我们来逐个看下可以引入模块的方式吧。

  • 引入整个模块,调用时需要加上模块名

    import calc
    
    print(calc.plus(1, 2)) # 3
    
    print(calc.subtract(2, 1)) # 1
    
  • 引入模块特定的函数或变量,调用时无需加模块名

    from calc import plus, subtract
    
    print(plus(1, 2))  # 3
    
    print(subtract(2, 1))  # 1
    
  • 引入整个模块,调用时无需加上模块名

    from calc import *
    
    print(plus(1, 2))  # 3
    
    print(subtract(2, 1))  # 1
    
  • 引入整个模块,并对模块重命名,调用时加上重命名后的模块名

    import calc as calculator
    
    print(calculator.plus(1, 2))  # 3
    
    print(calculator.subtract(2, 1))  # 1
    
  • 引入模块特定的函数或变量,并对其重命名,调用时无需加模块名

    from calc import plus as add, subtract as sub
    
    print(add(1, 2))  # 3
    
    print(sub(2, 1))  # 1
    

    数一下,一共是 6 种方式,归纳一下就是 from , import , as , * 这些符号的组合而已。

模块的查找顺序

在上几篇文章中已经用了如 osshutilsjson 等多个模块 ,这些模块都是 python 的内置模块。相比之下,我们刚才使用的 calc 模块就是自定义模块。

假设我们使用 import calc 导入 calc 模块, python 在启动时按照什么样的顺序来查找这个模块呢?

  1. 先查找内置(built-in)模块中有没有,如果没有转到 2
  2. 查找 sys.path 变量指定的路径下有没有, 有的话就使用,没有就报错

sys.path 变量中存储了那些路径呢?

  1. 当前运行的 python 脚本所在的目录

  2. 环境变量 PYTHONPATH 中的路径,它和 shell 环境变量 PATH 差不多

    这个变量可以使用 python 脚本在运行时修改它

  3. 默认的 python 安装包的路径

想要看下你的电脑当前 sys.path 有哪些路径吗?运行下面代码就可以

import sys
print(sys.path)

查找模块的顺序是从前向后,只要查到就使用,因此这个变量存储路径的顺序很重要。

模块中包含执行语句的情况

如果引入的模块中包含一些执行语句,那么在导入模块时这些语句就会执行。但是即使同样的模块被导入了两次,这些语句也只能执行一次。

来看下面的例子, 定义 calc 模块

print('I am clac module')


def plus(a, b):
    return a + b


def subtract(a, b):
    return a - b

并且在 main.py 中定义导入两次 calc 模块的函数

from calc import plus
from calc import subtract


print(plus(1, 2))
print(subtract(1, 2))

结果是 'I am clac module' 只会被打印一次。

用 dir() 函数来窥探模块

dir() 函数是 python 的内置函数,可用来获取模块的属性,方法等信息,当我们刚接触一个模块,不清楚它由哪些有用的属性和方法时,就可以用 dir() 来一探究竟。

以常用的 json模块 为例,我们来展示下它的属性和方法

import json

print(dir(json))
# ['JSONDecodeError', 'JSONDecoder', 'JSONEncoder', '__all__', '__author__', '__builtins__', '__cached__', '__doc__', '__file__', '__loader__', '__name__', '__package__', '__path__', '__spec__', '__version__', '_default_decoder', '_default_encoder', 'codecs', 'decoder', 'detect_encoding', 'dump', 'dumps', 'encoder', 'load', 'loads', 'scanner']

其中以双下划线开头的变量,如 __name__ 并非是模块自己定义的,而是与模块相关的默认属性。

如果我想查看当前模块内的所有属性和方法呢?去掉 dir() 函数的参数就可以。拿上节的代码为例来看下。

from calc import plus
from calc import subtract


print(plus(1, 2))
print(subtract(1, 2))
print(dir())
# ['__annotations__', '__builtins__', '__cached__', '__doc__', '__file__', '__loader__', '__name__', '__package__', '__spec__', 'plus', 'subtract']

我们会看到 calc 模块的 plussubstract 方法也展示了出来,那么 dir 函数究竟是从哪里获取的数据,背后的机理是什么呢?

其实每个模块内部都有一个子集的私有符号表,它就是模块内所有函数和方法共享的全局符号表。当模块 B 导入模块 A 时,就会把要导入的模块 A 或者特定的方法,属性放置到模块 B 的全局符号表中,dir() 函数也就是从模块中的全局符号表中获取出的值。

python 的内置模块有哪些?

python 的内置模块太丰富了,几乎可以满足我们日常的任何需求。既然有轮子就在那里,而且这轮子又快又好,又何必再造轮子呢?快来看下它的常用内置模块有哪些。

模块名 功能简述
calendar 与日期相关
datetime 处理日期和时间
csv 读写 csv 文件
json 读写 json 格式数据
collections 提供有用的数据结构
io 处理 I/O 流
os 基本的操作系统函数访问
shutil 高级文件处理
tempfile 创建临时文件和目录
logging 日志功能
random 生成伪随机数
copy 复制数据相关
codec 编解码
re 正则表达式
uuid 全局唯一标识符 (UUID)
multiprocessing 运行多个子进程
threading 线程
concurrent 异步
argparse 解析命令行参数
atexit 注册在程序退出时调用的函数
signal 处理 POSIX 信号

光常用的模块就这么一大堆,确实是很难都记住,记不住也没关系,当需要用到的时候随用随查就可以了。

结语

本篇中主要介绍了模块化的定义,引入模块化的方式,模块的查找顺序,常用的内置模块简介等内容,通过使用模块化,能够更好的实践模块化程序设计的思想。但本篇并没有涉及 package 的概念,会在后续章节讲述。

下篇会讲述 python 中正则表达式,敬请期待。

参考文档

  1. python modules
  2. 模块化程序设计—百度百科
  3. 《代码大全》
  4. 《Python高手之路》

系列文章列表

posted @ 2018-11-19 06:53  ReyCG  阅读(1300)  评论(0编辑  收藏  举报