14 Python的模块与包

一、模块介绍

1.1 什么是模块

模块就是一组功能的集合体,我们的程序可以导入模块来复用模块里的功能,分为三大类

  1. 内置的模块
  2. 第三方的模块
  3. 自定义的模块

在Python中,一个py文件就是一个模块,文件名为xxx.py模块名则是xxx,导入模块可以引用模块中已经写好的功能

1.2 为何要用模块

  1. 从文件级别组织程序,更方便管理

随着程序的发展,功能越来越多,为了方便管理,我们通常将程序分成一个个的文件,这样做程序的结构更清晰,方便管理。这时我们不仅仅可以把这些文件当做脚本去执行,还可以把他们当做模块来导入到其他的模块中,实现了功能的重复利用

  1. 拿来主义,提升开发效率

同样的原理,我们也可以下载别人写好的模块然后导入到自己的项目中使用,这种拿来主义,可以极大地提升我们的开发效率

如果你退出python解释器然后重新进入,那么你之前定义的函数或者变量都将丢失,因此我们通常将程序写到文件中以便永久保存下来,需要时就通过python test.py方式去执行,此时test.py被称为脚本script。

二、模块的使用

2.1 import语句

要想在另外一个py文件中引用foo.py中的功能,需要使用import foo,首次导入模块会做三件事:

  1. 执行foo.py源文件代码
  2. 产生foo.py的名称空间,将foo.py运行过程中产生的名字都丢到foo的名称空间中
  3. 在当前执行文件所在的名称空间中得到一个名字foo,该名字指向新创建的模块名称空间,若要引用模块名称空间中的名字,需要加上该前缀,如下
#文件名:foo.py
x=1
def get():
    print(x)
def change():
    global x
    x=0
class Foo:
    def func(self):
       print('from the func')
import foo #导入模块foo
a=foo.x #引用模块foo中变量x的值赋值给当前名称空间中的名字a
foo.get() #调用模块foo的get函数
foo.change() #调用模块foo中的change函数
obj=foo.Foo() #使用模块foo的类Foo来实例化,进一步可以执行obj.func()

加上foo.作为前缀就相当于指名道姓地说明要引用foo名称空间中的名字,所以肯定不会与当前执行文件所在名称空间中的名字相冲突,并且若当前执行文件的名称空间中存在x,执行foo.get()或foo.change()操作的都是源文件中的全局变量x

需要强调一点是,第一次导入模块已经将其加载到内存空间了,之后的重复导入会直接引用内存中已存在的模块,不会重复执行文件,通过import sys,打印sys.modules的值可以看到内存中已经加载的模块名

用import语句导入多个模块,可以写多行import语句

import module1
import module2
    ...
import moduleN

还可以在一行导入,用逗号分隔开不同的模块

import module1,module2,...,moduleN

但其实第一种形式更为规范,可读性更强,推荐使用,而且我们导入的模块中可能包含有python内置的模块、第三方的模块、自定义的模块,为了便于明显地区分它们,我们通常在文件的开头导入模块,并且分类导入,一类模块的导入与另外一类的导入用空行隔开,不同类别的导入顺序如下:

#1. python内置模块

#2. 第三方模块

#3. 程序员自定义模块

当然,我们也可以在函数内导入模块,对比在文件开头导入模块属于全局作用域,在函数内导入的模块则属于局部的作用域

2.2 from-import 语句

from…import…与import语句基本一致,唯一不同的是:使用import foo导入模块后,引用模块中的名字都需要加上foo.作为前缀,而使用from foo import x,get,change,Foo则可以在当前执行文件中直接引用模块foo中的名字,如下

from foo import x,get,change #将模块foo中的x和get导入到当前名称空间
a=x #直接使用模块foo中的x赋值给a
get() #直接执行foo中的get函数
change() #即便是当前有重名的x,修改的仍然是源文件中的x

无需加前缀的好处是使得我们的代码更加简洁,坏处则是容易与当前名称空间中的名字冲突,如果当前名称空间存在相同的名字,则后定义的名字会覆盖之前定义的名字

另外from语句支持from foo import *语法,*代表将foo中所有的名字都导入到当前位置

from foo import * 
#把foo中不是以下划线(_)开头的名字都导入到当前执行文件的名称空间中,在当前位置直接可以使用这些名字

a=x
get()
change()
obj=Foo()

如果我们需要引用模块中的名字过多的话,可以采用上述的导入形式来达到节省代码量的效果,但是需要强调的一点是:只能在模块最顶层使用的方式导入,在函数内则非法,并且的方式会带来一种副作用,即我们无法搞清楚究竟从源文件中导入了哪些名字到当前位置,这极有可能与当前位置的名字产生冲突。

大部分情况下我们的python程序不应该使用这种导入方式,因为*你不知道你导入什么名字很有可能会覆盖掉你之前已经定义的名字。而且可读性极其的差,在交互式环境中导入时没有问题

模块的编写者可以在自己的文件中定义__all__变量用来控制*代表的意思

#foo.py
__all__=['x','get'] #该列表中所有的元素必须是字符串类型,每个元素对应foo.py中的一个名字
x=1
def get():
    print(x)
def change():
    global x
    x=0
class Foo:
    def func(self):
       print('from the func')

这样我们在另外一个文件中使用*导入时,就只能导入__all__定义的名字了

from foo import * #此时的*只代表x和get

x #可用
get() #可用
change() #不可用
Foo() #不可用

再次声明,__all__变量仅限于在其它文件中以“from 模块名 import *”的方式引入。

2.3 其他导入语法(as)

我们还可以在当前位置为导入的模块起一个别名

import foo as f 
#为导入的模块foo在当前位置起别名f,以后再使用时就用这个别名f
f.x
f.get()

还可以为导入的一个名字起别名

from foo import get as get_x
get_x()

通常在被导入的名字过长时采用起别名的方式来精简代码,另外为被导入的名字起别名可以很好地避免与当前名字发生冲突,还有很重要的一点就是:可以保持调用方式的一致性,例如我们有两个模块json和pickle同时实现了load方法,作用是从一个打开的文件中解析出结构化的数据,但解析的格式不同,可以用下述代码有选择性地加载不同的模块

if data_format == 'json':
    import json as serialize #如果数据格式是json,那么导入json模块并命名为serialize
elif data_format == 'pickle':
    import pickle as serialize #如果数据格式是pickle,那么导入pickle模块并命名为serialize

data=serialize.load(fn) #最终调用的方式是一致的

2.4 循环导入问题

循环导入问题指的是在一个模块加载/导入的过程中导入另外一个模块,而在另外一个模块中又返回来导入第一个模块中的名字,由于第一个模块尚未加载完毕,所以引用失败、抛出异常,究其根源就是在python中,同一个模块只会在第一次导入时执行其内部代码,再次导入该模块时,即便是该模块尚未完全加载完毕也不会去重复执行内部代码

我们以下述文件为例,来详细分析循环/嵌套导入出现异常的原因以及解决的方案

# m1.py
print('正在导入m1')
from m2 import y

x='m1'
# m2.py
print('正在导入m2')
from m1 import x

y='m2'
# run.py
import m1

解决方案

# 方案一:导入语句放到最后,保证在导入时,所有名字都已经加载过
# 文件:m1.py
print('正在导入m1')

x='m1'

from m2 import y

# 文件:m2.py
print('正在导入m2')
y='m2'

from m1 import x

# 文件:run.py内容如下,执行该文件,可以正常使用
import m1
print(m1.x)
print(m1.y)

# 方案二:导入语句放到函数中,只有在调用函数时才会执行其内部代码
# 文件:m1.py
print('正在导入m1')

def f1():
    from m2 import y
    print(x,y)

x = 'm1'

# 文件:m2.py
print('正在导入m2')

def f2():
    from m1 import x
    print(x,y)

y = 'm2'

# 文件:run.py内容如下,执行该文件,可以正常使用
import m1

m1.f1()

注意:循环导入问题大多数情况是因为程序设计失误导致,上述解决方案也只是在烂设计之上的无奈之举,在我们的程序中应该尽量避免出现循环/嵌套导入,如果多个模块确实都需要共享某些数据,可以将共享的数据集中存放到某一个地方,然后进行导入

在程序出现了循环/嵌套导入后的异常分析、解决方法如下

  1. 导入语句放到最后
  2. 导入语句放到函数中

2.5 搜索模块的路径与优先级

无论是import还是from … import 在导入模块时都涉及到查找问题

在导入一个模块时,如果该模块已加载到内存中,则直接引用,否则会优先查找内置模块,然后按照从左到右的顺序依次检索sys.path中定义的路径,直到找模块对应的文件为止,否则抛出异常。sys.path也被称为模块的搜索路径,它是一个列表类型

import sys
print(sys.path)

列表中的每个元素其实都可以当作一个目录来看:在列表中会发现有.zip或.egg结尾的文件,二者是不同形式的压缩文件,事实上Python确实支持从一个压缩文件中导入模块,我们也只需要把它们都当成目录去看即可

sys.path中的第一个路径通常为空,代表执行文件所在的路径,所以在被导入模块与执行文件在同一目录下时肯定是可以正常导入的,而针对被导入的模块与执行文件在不同路径下的情况,为了确保模块对应的源文件仍可以被找到,需要将源文件foo.py所在的路径添加到sys.path中,假设foo.py所在的路径为/pythoner/projects/

import sys
sys.path.append(r'/pythoner/projects/') #也可以使用sys.path.insert(……)

import foo #无论foo.py在何处,我们都可以导入它了

tips:第二个值是pycharm自动帮你加的,不能为准

优先从内存找,可以通过实验,导入模块打印一个值,sleep几秒钟,趁机删除模块文件,看能不能继续打印出模块文件的输出

了解:sys.modules查看已经加载到内存中的模块

2.6 区分py文件的两种用途

一个Python文件有两种用途

  1. 被当主程序/脚本执行
  2. 被当模块导入

为了区别同一个文件的不同用途,每个py文件都内置了__name__变量,该变量在py文件被当做脚本执行时赋值为“__main__”,在py文件被当做模块导入时赋值为模块名

作为模块foo.py的开发者,可以在文件末尾基于__name__在不同应用场景下值的不同来控制文件执行不同的逻辑

#foo.py
...
if __name__ == '__main__':
    foo.py被当做脚本执行时运行的代码
else:
    foo.py被当做模块导入时运行的代码

通常我们会在if的子代码块中编写针对模块功能的测试代码,这样foo.py在被当做脚本运行时,就会执行测试代码,而被当做模块导入时则不用执行测试代码

2.7 编写一个规范的模块

我们在编写py文件时,需要时刻提醒自己,该文件既是给自己用的,也有可能会被其他人使用,因而代码的可读性与易维护性显得十分重要,为此我们在编写一个模块时最好按照统一的规范去编写,如下

#!/usr/bin/env python #通常只在类unix环境有效,作用是可以使用脚本名来执行,而无需直接调用解释器。

"The module is used to..." #模块的文档描述

import sys #导入模块

x=1 #定义全局变量,如果非必须,则最好使用局部变量,这样可以提高代码的易维护性,并且可以节省内存提高性能

class Foo: #定义类,并写好类的注释
    'Class Foo is used to...'
    pass

def test(): #定义函数,并写好函数的注释
    'Function test is used to…'
    pass

if __name__ == '__main__': #主程序
    test() #在被当做脚本执行时,执行此处的代码

三、包

实际开发中,一个大型的项目往往需要使用成百上千的 Python模块,如果将这些模块都堆放在一起,势必不好管理。而且,使用模块可以有效避免变量名或函数名重名引发的冲突,但是如果模块名重复怎么办呢?因此,Python提出了包(Package)的概念。

什么是包呢?简单理解,包就是文件夹,只不过在该文件夹下必须存在一个名为“__init__.py” 的文件。

注意,这是 Python 2.x 的规定,而在 Python 3.x 中,__init__.py对包来说,并不是必须的

每个包的目录下都必须建立一个 __init__.py 的模块,可以是一个空模块,可以写一些初始化代码,其作用就是告诉 Python 要将该目录当成包来处理

注意,__init__.py 不同于其他模块文件,此模块的模块名不是 __init__,而是它所在的包名。例如,在 settings 包中的 __init__.py 文件,其模块名就是 settings

包是一个包含多个模块的文件夹,它的本质依然是模块,因此包中也可以包含包。

3.1 创建包

包其实就是文件夹,更确切的说,是一个包含“__init__.py”文件的文件夹。因此,如果我们想手动创建一个包,只需进行以下 2 步操作:

  1. 新建一个文件夹,文件夹的名称就是新建包的包名
  2. 在该文件夹中,创建一个 __init__.py 文件(前后各有 2 个下划线‘_’),该文件中可以不编写任何代码。当然,也可以编写一些python初始化代码,则当有其它程序文件导入包时,会自动执行该文件中的代码

例如,现在我们创建一个非常简单的包,该包的名称为 my_package,可以仿照以上 2 步进行:

  1. 创建一个文件夹,其名称设置为 my_package

  2. 在该文件夹中添加一个 __init__.py文件,此文件中可以不编写任何代码。不过,这里向该文件编写如下代码:

    '''
    这是一个包
    '''
    print('这是一个包')
    

    可以看到,__init__.py文件中,包含了 2 部分信息,分别是此包的说明信息和一条 print 输出语句

由此,我们就成功创建好了一个 Python 包

创建好包之后,我们就可以向包中添加模块(也可以添加包)。这里给 my_package 包添加 2 个模块,分别是 module1.py、module2.py,各自包含的代码分别如下所示

#module.py模块文件
def display(arc):
    print(arc)

现在,我们就创建好了一个具有如下文件结构的包:

my_package
     ┠── __init__.py
     ┠── module.py

3.2 Python包的导入

通过前面的学习我们知道,包其实本质上还是模块,因此导入模块的语法同样也适用于导入包。无论导入我们自定义的包,还是导入从他处下载的第三方包,导入方法可归结为以下 3 种:

  1. import 包名[.模块名 [as 别名]]
  2. from 包名 import 模块名 [as 别名]
  3. from 包名.模块名 import 成员名 [as 别名]

用 [] 括起来的部分,是可选部分,即可以使用,也可以直接忽略

1) import 包名[.模块名 [as 别名]]

以前面创建好的 my_package 包为例,导入 module 模块并使用该模块中成员可以使用如下代码:

import my_package.module

my_package.module.display("python")

可以看到,通过此语法格式导入包中的指定模块后,在使用该模块中的成员(变量、函数、类)时,需添加“包名.模块名”为前缀。当然,如果使用 as 给包名.模块名”起一个别名的话,就使用直接使用这个别名作为前缀使用该模块中的方法了

当直接导入指定包时,程序会自动执行该包所对应文件夹下的 __init__.py文件中的代码

直接导入包名,并不会将包中所有模块全部导入到程序中,它的作用仅仅是导入并执行包下的 __init__.py 文件,因此,运行该程序,在执行 __init__.py文件中代码的同时,还会抛出 AttributeError 异常(访问的对象不存在)

2) from 包名 import 模块名 [as 别名]

仍以导入 my_package 包中的 module 模块为例,使用此语法格式的实现代码如下

from my_package import module
module1.display("python")

可以看到,使用此语法格式导入包中模块后,在使用其成员时不需要带包名前缀,但需要带模块名前缀

当然,我们也可以使用 as 为导入的指定模块定义别名

同样,既然包也是模块,那么这种语法格式自然也支持 from 包名 import * 这种写法,它和 import 包名 的作用一样,都只是将该包的 __init__.py文件导入并执行

3) from 包名.模块名 import 成员名 [as 别名]

此语法格式用于向程序中导入“包.模块”中的指定成员(变量、函数或类)。通过该方式导入的变量(函数、类),在使用时可以直接使用变量名(函数名、类名)调用,例如:

from my_package.module import display
display("python")

当然,也可以使用 as 为导入的成员起一个别名

另外,在使用此种语法格式加载指定包的指定模块时,可以使用 * 代替成员名,表示加载该模块下的所有成员

from my_package.module import *
display("http://c.biancheng.net/python")

3.2 绝对导入与相对导入

针对包内的模块之间互相导入,导入的方式有两种

  1. 绝对导入:以顶级包为起始

    #pool下的__init__.py
    from pool import versions
    
  2. 相对导入:.代表当前文件所在的目录,…代表当前目录的上一级目录,依此类推

    #pool下的__init__.py
    from . import versions
    

    在包内使用相对导入还可以跨目录导入模块

    import也能使用绝对导入,导入过程中同样会依次执行包下的__init__.py,只是基于import导入的结果,使用时必须加上该前缀

    针对包内部模块之间的相互导入推荐使用相对导入,需要特别强调:

    1. 相对导入只能在包内部使用,用相对导入不同目录下的模块是非法的
    2. 无论是import还是from-import,但凡是在导入时带点的,点的左边必须是包,否则语法错误

总结包的使用需要牢记三点

  1. 导包就是在导包下__init__.py文件
  2. 包内部的导入应该使用相对导入,相对导入也只能在包内部使用,而且…取上一级不能出包
  3. 使用语句中的点代表的是访问属性 如:m.n.x ----> 向m要n,向n要x
  4. 导入语句中的点代表的是路径分隔符 如:import a.b.c --> a/b/c,文件夹下a下有子文件夹b,文件夹b下有子文件或文件夹c 。所以导入语句中点的左边必须是一个包

四、第三方库(模块)下载和安装

进行 Python 程序开发时,除了使用 Python 内置的标准模块以及我们自定义的模块之外,还有很多第三方模块可以使用,这些第三方模块可以借助 Python官方提供的查找包页面(https://pypi.org/)找到

使用第三方模块之前,需要先下载并安装该模块,然后就能像使用标准模块和自定义模块那样导入并使用了。因此,本节主要讲解如何下载并安装第三方模块。

下载和安装第三方模块,可以使用 Python 提供的 pip 命令实现。pip 命令的语法格式如下:

pip install|uninstall|list 模块名

其中,install、uninstall、list 是常用的命令参数,各自的含义为:

  1. install:用于安装第三方模块,当 pip 使用 install 作为参数时,后面的模块名不能省略
  2. uninstall:用于卸载已经安装的第三方模块,选择 uninstall 作为参数时,后面的模块名也不能省略
  3. list:用于显示已经安装的第三方模块

pip 命令会将下载完成的第三方模块,默认安装到 Python 安装目录中的 \Lib\site-packages 目录下。

对于向程序中导入模块,\Lib\site-packages 目录是 Python 肯定会搜索的目录,因此位于此目录的模块,可以直接使用 import 语句引入

通过 pip 命令,我们可以下载并安装很多第三方模块,如果想要查看 Python 中目前有哪些模块(包括标准模块和第三方模块),可以在 IDLE 中输入以下命令:

help('modules')

在此基础上,如果只是想要查看已经安装的第三方模块,可以在使用如下命令:

pip list

4.1 pip换源

在实际开发中, 可能要大量使用第三方模块(包), 更换至国内下载源, 可大幅提升下载速度

  1. 采用国内源,加速下载模块的速度
  2. 常用pip源:
    1. 豆瓣:https://pypi.douban.com/simple
    2. 阿里:https://mirrors.aliyun.com/pypi/simple
  3. 加速安装的命令:
    • pip install -i https://pypi.douban.com/simple 模块名

永久配置安装源

Windows
  1. 文件管理器文件路径地址栏敲:%APPDATA% 回车,快速进入 C:\Users\电脑用户\AppData\Roaming 文件夹中
  2. 新建 pip 文件夹并在文件夹中新建 pip.ini 配置文件
  3. 新增 pip.ini 配置文件内容
配置文件内容
[global]
index-url = http://pypi.douban.com/simple
[install]
use-mirrors =true
mirrors =http://pypi.douban.com/simple/
trusted-host =pypi.douban.com

五、软件开发的目录规范

为了提高程序的可读性与可维护性,我们应该为软件设计良好的目录结构,这与规范的编码风格同等重要。软件的目录规范并无硬性标准,只要清晰可读即可,假设你的软件名为foo,推荐目录结构如下

Foo/
|-- core/
|   |-- core.py
|
|-- api/
|   |-- api.py
|
|-- db/
|   |-- db_handle.py
|
|-- lib/
|   |-- common.py
|
|-- conf/
|   |-- settings.py
|
|-- run.py
|-- setup.py
|-- requirements.txt
|-- README

简要解释一下:

  • core/: 存放业务逻辑相关代码
  • api/: 存放接口文件,接口主要用于为业务逻辑提供数据操作
  • db/: 存放操作数据库相关文件,主要用于与数据库交互
  • lib/: 存放程序中常用的自定义模块
  • conf/: 存放配置文件
  • run.py: 程序的启动文件,一般放在项目的根目录下,因为在运行时会默认将运行文件所在的文件夹作为sys.path的第一个路径,这样就省去了处理环境变量的步骤
  • setup.py: 安装、部署、打包的脚本
  • requirements.txt: 存放软件依赖的外部Python包列表
  • README: 项目说明文件

关于README的内容,这个应该是每个项目都应该有的一个文件,目的是能简要描述该项目的信息,让读者快速了解这个项目。它需要说明以下几个事项:

  1. 软件定位,软件的基本功能
  2. 运行代码的方法: 安装环境、启动命令等
  3. 简要的使用说明
  4. 代码目录结构说明,更详细点可以说明软件的基本原理
  5. 常见问题说明

一般来说,用setup.py来管理代码的打包、安装、部署问题。业界标准的写法是用Python流行的打包工具setuptools来管理这些事情,这种方式普遍应用于开源项目中。不过这里的核心思想不是用标准化的工具来解决这些问题,而是说,一个项目一定要有一个安装部署工具,能快速便捷的在一台新机器上将环境装好、代码部署好和将程序运行起来。

requirements.txt文件的存在是为了方便开发者维护软件的依赖库。我们需要将开发过程中依赖库的信息添加进该文件中,避免在 setup.py安装依赖时漏掉软件包,同时也方便了使用者明确项目引用了哪些Python包。

快速生成requirement.txt的安装文件

pip freeze > requirements.txt

安装所需要的的文件

pip install -r requirement.txt

  • 9
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值