【8.0】Python高级之模块与包

【一】模块介绍

【1】什么是模块

  • 在Python中,一个py文件就是一个模块,文件名为xxx.py模块名则是xxx,导入模块可以引用模块中已经写好的功能。
    • 如果把开发程序比喻成制造一台电脑
    • 编写模块就像是在制造电脑的零部件
    • 准备好零部件后,剩下的工作就是按照逻辑把它们组装到一起。
  • 将程序模块化会使得程序的组织结构清晰,维护起来更加方便。
    • 比起直接开发一个完整的程序,单独开发一个小的模块也会更加简单,并且程序中的模块与电脑中的零部件稍微不同的是:程序中的模块可以被重复使用。
  • 所以总结下来,使用模块既保证了代码的重用性,又增强了程序的结构性和可维护性。
  • 另外除了自定义模块外,我们还可以导入使用内置或第三方模块提供的现成功能,这种“拿来主义”极大地提高了程序员的开发效率。

【2】模块的优点

  • 极大地提高了程序员的开发效率。

【3】模块的来源

  • 内置的:python解释器自带的,直接拿来使用的
  • 第三方的:别人写的,如果想用,就要先下载在使用
  • 自定义的:我们自己写的

【4】模块的存在形式

  • 我们自己写的py文件(一个py文件就是一个模块)
  • 包:一系列py文件的集合(文件夹)

一个包里面会有一个__init__.py文件

【二】模块的使用

【0】准备

  • 新建一个文件 foo.py
x = 1


# 定义一个普通函数
def get():
    print(x)


# 定义一个修改全局变量的函数
def change():
    global x
    x = 0


# 定义一个类,类里面有方法
class Foo:
    def func(self):
        print('the func from the foo')

【1】直接导入

  • import 语句
import py文件名/模块名

(1)import导入模块会发生哪些事

  • 要想在另外一个 py 文件中引用foo.py中的功能
  • 需要使用 import foo
  • 首次导入模块会做三件事:
    • 执行源文件代码
    • 产生一个新的名称空间用于存放源文件执行过程中产生的名字
    • 在当前执行文件所在的名称空间中得到一个名字 foo,该名字指向新创建的模块名称空间
    • 若要引用模块名称空间中的名字,需要加上该前缀
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的值可以看到内存中已经加载的模块名。
  • 1、在Python中模块也属于第一类对象

    • 可以进行赋值、以数据形式传递以及作为容器类型的元素等操作。
  • 2、模块名应该遵循小写形式,标准库从python2过渡到python3做出了很多这类调整

    • 比如ConfigParser、Queue、SocketServer全更新为纯小写形式。

(2)import导入模块方式

  • 用import语句导入多个模块

    • 可以写多行import语句
    import module1
    import module2
        ...
    import moduleN
    
  • 还可以在一行导入,用逗号分隔开不同的模块

    import module1,module2,...,moduleN
    
  • 但其实第一种形式更为规范,可读性更强,推荐使用
  • 而且我们导入的模块中可能包含有python内置的模块、第三方的模块、自定义的模块
  • 为了便于明显地区分它们,我们通常在文件的开头导入模块
  • 并且分类导入
  • 一类模块的导入与另外一类的导入用空行隔开
  • 不同类别的导入顺序如下:
  • python内置模块
  • 第三方模块
  • 程序员自定义模块
  • 当然,我们也可以在函数内导入模块,对比在文件开头导入模块属于全局作用域,在函数内导入的模块则属于局部的作用域。

【2】详细导入

  • from ... import ... 语句
from 模块位置 import 模块名

(1)语法

  • 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
    
  • 无需加前缀的

    • 好处是使得我们的代码更加简洁
    • 坏处则是容易与当前名称空间中的名字冲突
  • 如果当前名称空间存在相同的名字

    • 则后定义的名字会覆盖之前定义的名字。

(2)导入所有

  • 另外from语句支持 from foo import *语法, * 代表将foo中所有的名字都导入到当前位置
# 把foo中所有的名字都导入到当前执行文件的名称空间中,在当前位置直接可以使用这些名字
from foo import *  

a = x
get()
change()
obj = Foo()
  • 如果我们需要引用模块中的名字过多的话,可以采用上述的导入形式来达到节省代码量的效果
  • 但是需要强调的一点是:
    • 只能在模块最顶层使用 * 的方式导入,在函数内则非法
    • 并且*的方式会带来一种副作用
      • 即我们无法搞清楚究竟从源文件中导入了哪些名字到当前位置
      • 这极有可能与当前位置的名字产生冲突。
    • 模块的编写者可以在自己的文件中定义__all__变量用来控制 * 代表的意思
# foo.py

# 该列表中所有的元素必须是字符串类型,每个元素对应foo.py中的一个名字
__all__=['x','get'] 
x=1
def get():
    print(x)
def change():
    global x
    x=0
class Foo:
    def func(self):
       print('from the func')
  • 这样我们在另外一个文件中使用 * 导入时,就只能导入__all__定义的名字了
# 此时的*只代表x和get

from foo import * 

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

【3】模块重命名

  • from ... import ... as ... 语句
from 模块位置 import 模块名 as 自定义名字
  • 我们还可以在当前位置为导入的模块起一个别名
#为导入的模块foo在当前位置起别名f,以后再使用时就用这个别名f
import foo as f 
f.x
f.get()
  • 还可以为导入的一个名字起别名
from foo import get as get_x
get_x()
  • 通常在被导入的名字过长时采用起别名的方式来精简代码
    • 另外为被导入的名字起别名可以很好地避免与当前名字发生冲突
  • 还有很重要的一点就是:
    • 可以保持调用方式的一致性
  • 例如
    • 我们有两个模块json和pickle同时实现了load方法
      • 作用是从一个打开的文件中解析出结构化的数据
    • 但解析的格式不同
      • 可以用下述代码有选择性地加载不同的模块
if data_format == 'json':
    # 如果数据格式是json,那么导入json模块并命名为serialize
    import json as serialize 
elif data_format == 'pickle':
    # 如果数据格式是pickle,那么导入pickle模块并命名为serialize
    import pickle as serialize 
    
# 最终调用的方式是一致的
data=serialize.load(fn) 

【4】循环导入问题

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

(1)循环导入所引发的问题演示

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

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

y='m2'
  • run.py
import m1

[1]演示一

  • 执行run.py会抛出异常
正在导入m1
正在导入m2

Traceback (most recent call last):
  File "/Users/linhaifeng/PycharmProjects/pro01/1 aaaa练习目录/aa.py", line 1, in <module>
    import m1
  File "/Users/linhaifeng/PycharmProjects/pro01/1 aaaa练习目录/m1.py", line 2, in <module>
    from m2 import y
  File "/Users/linhaifeng/PycharmProjects/pro01/1 aaaa练习目录/m2.py", line 2, in <module>
    from m1 import x
ImportError: cannot import name 'x'
  • 分析
    • 先执行run.py--->执行import m1
    • 开始导入m1并运行其内部代码--->打印内容"正在导入m1"
    • --->执行from m2 import y 开始导入m2并运行其内部代码--->打印内容“正在导入m2”
    • --->执行from m1 import x,由于m1已经被导入过了,所以不会重新导入,所以直接去m1中拿x
    • 然而x此时并没有存在于m1中,所以报错

[2]演示二

  • 执行文件不等于导入文件,比如执行m1.py不等于导入了m
直接执行m1.py抛出异常
正在导入m1
正在导入m2
正在导入m1
Traceback (most recent call last):
  File "/Users/linhaifeng/PycharmProjects/pro01/1 aaaa练习目录/m1.py", line 2, in <module>
    from m2 import y
  File "/Users/linhaifeng/PycharmProjects/pro01/1 aaaa练习目录/m2.py", line 2, in <module>
    from m1 import x
  File "/Users/linhaifeng/PycharmProjects/pro01/1 aaaa练习目录/m1.py", line 2, in <module>
    from m2 import y
ImportError: cannot import name 'y'
  • 分析
    • 执行m1.py,打印“正在导入m1”,
    • 执行from m2 import y
      • 导入m2进而执行m2.py内部代码
      • --->打印"正在导入m2"
    • 执行from m1 import x
      • 此时m1是第一次被导入
      • 执行m1.py并不等于导入了m1
    • 于是开始导入m1并执行其内部代码
      • --->打印"正在导入m1"
    • 执行from m1 import y
      • 由于m1已经被导入过了
      • 所以无需继续导入而直接问m2要y
      • 然而y此时并没有存在于m2中所以报错

(2)循环引入产生的问题的解决方案

[1]方案一

  • 导入语句放到最后,保证在导入时,所有名字都已经加载过
  • 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)

[2]方案二

  • 导入语句放到函数中,只有在调用函数时才会执行其内部代码
  • 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】模块的分类

  • 模块其实分为四个通用类别,分别是:
    • 1、使用纯Python代码编写的py文件
    • 2、包含一系列模块的包
    • 3、使用C编写并链接到Python解释器中的内置模块
    • 4、使用C或C++编译的扩展模块
  • 在导入一个模块时
    • 如果该模块已加载到内存中,则直接引用
    • 否则会优先查找内置模块
      • 然后按照从左到右的顺序依次检索 sys.path 中定义的路径
    • 直到找模块对应的文件为止
      • 否则抛出异常。

【2】查看模块的搜索路径

  • sys.path 也被称为模块的搜索路径,它是一个列表类型
import sys

print(sys.path)
'''
[
'E:\\PythonProjects\\04模块与包', 
'E:\\PythonProjects', 
'E:\\Pycharm\\PyCharm 2023.2\\plugins\\python\\helpers\\pycharm_display', 
'E:\\Python310\\python310.zip', 
'E:\\Python310\\DLLs', 
'E:\\Python310\\lib', 
'E:\\Python310', 
'E:\\Python310\\lib\\site-packages', 
'E:\\Pycharm\\PyCharm2023.2\\plugins\\python\\helpers\\pycharm_matplotlib_backend'
]
'''
  • 列表中的每个元素其实都可以当作一个目录来看:

    • 在列表中会发现有.zip或.egg结尾的文件
      • 二者是不同形式的压缩文件
  • 事实上Python确实支持从一个压缩文件中导入模块

    • 我们也只需要把它们都当成目录去看即可。
  • sys.path 中的第一个路径通常为空,代表执行文件所在的路径,所以在被导入模块与执行文件在同一目录下时肯定是可以正常导入的

  • 而针对被导入的模块与执行文件在不同路径下的情况

    • 为了确保模块对应的源文件仍可以被找到
    • 需要将源文件 foo.py 所在的路径添加到 sys.path
    • 假设 foo.py 所在的路径为 /pythoner/projects/
    import sys
    
    # 也可以使用sys.path.insert(……)
    sys.path.append(r'/pythoner/projects/') 
    
    # 无论foo.py在何处,我们都可以导入它了
    import foo 
    
    • 作为模块foo.py的开发者
      • 可以在文件末尾基于__name__在不同应用场景下值的不同来控制文件执行不同的逻辑
    # foo.py
    ...
    if __name__ == '__main__':
    	# 被当做脚本执行时运行的代码
        foo.py
    else:
        # 被当做模块导入时运行的代码
        foo.py
    

    通常我们会在if的子代码块中编写针对模块功能的测试代码

    这样foo.py在被当做脚本运行时,就会执行测试代码

    而被当做模块导入时则不用执行测试代码。

【6】模块编写规范

  • 我们在编写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() # 在被当做脚本执行时,执行此处的代码

【四】模块小结

【1】什么是模块

  • 常见的场景:
    • 一个模块就是一个包含了python定义和声明的文件
    • 文件名就是模块名字加上.py的后缀。
  • 但其实import加载的模块分为四个通用类别:
    • 1 使用python编写的代码(.py文件)
    • 2 已被编译为共享库或DLL的C或C++扩展
    • 3 包好一组模块的包
    • 4 使用C编写并链接到python解释器的内置模块

【2】模块的介绍与使用(见上文)

【3】模块的优点

  • 如果你退出python解释器然后重新进入,那么你之前定义的函数或者变量都将丢失
    • 因此我们通常将程序写到文件中以便永久保存下来
    • 需要时就通过python test.py方式去执行
    • 此时test.py被称为脚本script。
  • 随着程序的发展,功能越来越多,为了方便管理,我们通常将程序分成一个个的文件,这样做程序的结构更清晰,方便管理。
    • 这时我们不仅仅可以把这些文件当做脚本去执行
    • 还可以把他们当做模块来导入到其他的模块中,实现了功能的重复利用,

【五】包

【1】什么是包

  • 包是一个模块的集合,它可以将多个模块的功能组合到一起。
  • 包可以按照其功能和作用进行分类,方便用户查找和使用。
  • 包是在Python标准库中定义的一种特殊类型的模块,可以通过import语句来引入和使用。
  • Python的包分为标准库包和第三方库包。
    • 标准库包是Python内置的包,包含了一些基本的模块和函数,如os、sys、random等;
    • 第三方库包是第三方开发的包,通常提供了更加丰富和高级的功能。

【2】包结构

  • 包是Python程序中用于组织模块的一种方式。包是一个包含模块的目录,同时还可以包含其他子包。
  • 要创建一个包,我们只需要在目录中创建一个名为__init__.py的文件即可。
|-pakageName
	|-__init__.py
	|-moduleName1.py
	|-moduleName2.py
	|-...
  • 包路径下必须存在 __init__.py 文件。

【3】创建包

  • 我们创建一个 cal 的包,包中有一个计算器的 model ,结构如下:
|-cal
	|-__init__.py
	|-calculator.py
  • calculator.py
def add(a, b):
    return a + b


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


def multiply(a, b):
    return a * b


def divide(a, b):
    return a / b

image-20231120154310330

【4】直接使用包

(1)语法

  • Python 包的使用和模块的使用类似,下面是导入的语法:
import 包名.包名.模块名

(2)演示

  • 比如我们在 use_cal.py 中导入 calculator.py
# 导入包
import cal.calculator
# 使用包的模块的方法
print(cal.calculator.add(1,2))

image-20231120154519670

【5】详细使用包

(1)语法

  • 但是导入调用的时候报名比较长,这样就可以使用from ... import ...语句来简化一下。
from 包名.模块名 import 模块中的方法

(2)使用

  • use_cal.py 中导入 calculator.py
# 导入包
from cal import calculator

# 使用包的模块的方法
print(calculator.multiply(3, 6))

image-20231120154753272

【6】制作包

(1)注册包

  • 在包下的__init__.py中注册相关变量
  • __init__.py
# 注意这里要用相对路径
# 从 calculator.py 中将需要导出的方法导入到这里
from .calculator import add, reduce

(2)使用包

  • use_cal.py
# 将注册在 __init__.py 中的方法导入过来
from cal import add, reduce

# 使用包的模块的方法
print(add(3, 6))

image-20231120155320069

【六】绝对路径和相对路径

  • 在Python中,路径分为相对路径和绝对路径。

【1】相对路径

  • 相对路径是相对于当前工作目录或当前脚本文件所在目录的路径。
  • 使用相对路径时,你指定的路径是相对于执行脚本的当前工作目录的。
# 例如,假设当前工作目录是 '/Users/username/projects',你可以使用相对路径如下:
relative_path = 'data/file.txt'

【2】绝对路径

  • 绝对路径是文件或目录在文件系统中的完整路径,不依赖于当前工作目录。
  • 使用绝对路径时,你指定的路径是从文件系统的根目录开始的。
# 例如,绝对路径可能是:
absolute_path = '/Users/username/projects/data/file.txt'

【3】案例说明

  • 假设有以下文件结构:
project/
|-- scripts/
|   |-- my_script.py
|-- data/
    |-- file.txt

(1)相对路径

  • 如果 my_script.py 中需要访问 file.txt,可以使用相对路径
# my_script.py
relative_path = '../data/file.txt'

with open(relative_path, 'r') as file:
    content = file.read()
    print(content)
  • 在这个例子中,'../data/file.txt' 是相对于 my_script.py 所在目录的相对路径。

(2)绝对路径

  • 如果使用绝对路径
# my_script.py
absolute_path = '/Users/username/projects/data/file.txt'

with open(absolute_path, 'r') as file:
    content = file.read()
    print(content)
  • 在这个例子中,'/Users/username/projects/data/file.txt' 是文件系统的绝对路径。
posted @ 2023-11-20 16:09  Chimengmeng  阅读(222)  评论(0编辑  收藏  举报
/* */