python中的模块和包

时间:2023-03-08 16:06:20

模块

一 什么是模块

模块就是一组功能的集合体,可以通过导入模块来复用模块的功能。

比如我在同一个文件夹定义两个.py文件,分别命名为A.py和B.py,那么可以通过在A文件里通过import B来使用B文件里的名称空间。

python中,模块的使用方式都是一样的,可以分为四个通用类别:

  1. 使用python编写的.py文件
  2. 已被编译为共享库或DLL的C或C++扩展
  3. 把一系列模块组织到一起的文件夹(注:文件夹下有一个__init__.py文件,该文件夹称之为包)
  4. 使用C编写并链接到python解释器的内置模块

二 为何要使用模块

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

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

  2. 使用别人写好的模块,提高开发效率

    使用别人已经写好的*,在自己的项目中使用,可以极大地提高开发效率。

    注意:当退出python解释器的时候,重新进入那么之前定义的函数和变量都会丢失,因此通常将程序写到文件中以便永久保存,需要时可以在命令行通过python *.py方法执行。

# test1.py
print('from test1') money = 10
def func1():
print('test1模块:', money) def func2():
print('test1模块')
func1() def change():
global money
money = 100
# test2.py
import test1
test1.func1()
# 运行结果
from test1
test1模块: 10

当在tes2t导入的时候会执行test1中的代码,所以首先会打印from test1接着执行test1中的func1函数。

三 使用模块之import

1 import的使用

模块可以包含可执行的语句和函数的定义,这些语句的目的是初始化模块,它们只在模块名第一次导入时才会执行(import语句可以在程序中的任意位置使用的,且针对同一个模块可以import多次,python为了防止重复导入,当第一次导入模块时就将模块的名称空间加载到内存了,后续的import语句仅是对已经加载到内存中的模块对象增加了一次引用,并不会重复执行模块内的语句)

# test3
print('from test3')
# test4
import test3
import test3
import test3
import test3
# 运行结果
from test3

ps:可以导入sys模块,调用sys.module查看当前加载到内存中的模块,sys.module是一个字典,内部包含模块名和模块名路径的对应关系。该字典决定了导入模块时是否需要重新导入。

2 导入模块时发生了什么

运行py文件导入一个模块时,解释器做了三件事:

  1. 在执行文件中为被导入文件创建新的名称空间,在被导入模块中定义的函数和方法若是使用到了global时访问的就是这个名称空间。

  2. 在新创建的名称空间中执行模块中的包含的代码。函数定义也是‘’被执行‘’的语句,模块级别函数定义的执行将函数名放入模块全局名称空间表,可以用globals()可以查看。

  3. 在执行文件的名称空间创建被导入模块的名称来引用该名称空间。

    这个名字和变量名没什么区别,都是‘’第一类‘’的,且使用句点表示法可以访问被导入模块的名称空间,导入的模块与被导入的模块是独立的两个名称空间。

3 被导入模块有独立的名称空间

每个模块都是一个独立的名称空间,定义在这个模块中的函数,把这个模块的名称空间当做全局名称空间,这样就不用担心定义在不同的模块中全局变量在导入时与使用者的全局变量冲突。

  • 测试1
# test5.py
import test1
money = 100
print(test1.money)
# 运行结果
from test1
10
  • 测试2
# test6
import test1
def func1():
print('from test6 func1')
test1.func1()
func1()
# 运行结果
from test1
test1模块: 10
from test6 func1
# 这说明 test6中的函数和test1中的函数不冲突
  • 测试3
# test7
import test1
money = 1
test1.change()
print(money)
# 运行结果
from test1
test1模块: 10
1
# 这说明test1中的change函数只是修改了test1中的全局变量,对test7中的变量没有操作权限

四 使用模块之from...import...

1 from...import…与import ...的区别

区别就是:使用from...import…是将被导入模块中的名字直接导入到当前的名称空间中,所以在当前名称空间中,直接使用名字就可以了,不需要在前面加上模块名前缀。

from...import…导入方式的优缺点:

  • 好处:使用方便
  • 坏处:容易与当前执行文件的名字冲突

2 验证

  1. 验证1:当前位置直接使用test1中的函数名,执行时仍然以test1.py文件为全局名称空间
# 导入的函数func1,执行时仍然回到test1.py中寻找全局变量money
# test8
from test1 import func1
money = 50
func1()
# 运行结果
from test1
test1模块: 10
# 导入的函数func2,执行时需要调用func1,仍然回到test1.py中找func1
#test9.py
from test1 import func2
def func1():
print('-----')
func2()
# 运行结果
from test1
test1模块
test1模块: 10
  1. 验证2:如果当前名称空间和被导入的模块中的名字重合,那么会覆盖掉前面的名字
# 导入的函数fun1,被当前位置定义的func1覆盖掉了
# test10
from test1 import func1
def func1():
print('======')
func1()
# 运行结果
from test1
======
# 这说明func1把被导入名字func1覆盖掉了
# 当前位置定义的func1导入func1
# test11
def func1():
print('======')
from test1 import func1
func1()
# 运行结果
from test1
test1模块: 10
# 这说明在后面导入的话会覆盖掉前面的
  1. 验证3:被导入的方法执行时,始终以源文件为准
# test12
from test1 import money,func1
money=100 #将当前位置的名字money绑定到了100
print(money) #打印当前的名字
func1() #读取spam.py中的名字money,仍然为1000
# 运行结果
from the test
100
test1模块: 10
# 可以看出运行func1的时候依然从原名称空间查找的

3 from ... import *

from ... import * 是把被导入文件中所有不是以下划线(_)开头的名字都导入到当前名称空间。

大部分情况下不应该以这种导入方式,因为不知道被导入包中的名字是否会和当前名称空间中的名字重合造成名字覆盖。

解决方法是在被导入文件中使用__all__ = []来控制被导入的名字,只有在__all__里面的才会被导入。

4 模块循环导入问题

模块循环/嵌套导入抛出异常的根本原因是由于在python中模块被导入一次之后,就不会重新导入。

# test1.py
print('正在导入1')
from test2 import y
x = '1'
# test2.py
print('正在导入2')
from test1 import x
y = '2'
# run.py
import test1
# 运行结果
正在导入1
正在导入2
Traceback (most recent call last):
File "/Users/jingxing/PycharmProjects/python全栈/day18/pack/run.py", line 6, in <module>
import test1
File "/Users/jingxing/PycharmProjects/python全栈/day18/pack/test1.py", line 7, in <module>
from test2 import y
File "/Users/jingxing/PycharmProjects/python全栈/day18/pack/test2.py", line 8, in <module>
from test1 import x
ImportError: cannot import name 'x'

分析:在run文件中执行导入test1,运行test1的代码,打印并且从test2中导入y,回到test2,打印并且从test1中导入,因为已经导入test1了(没导入完全,因为代码没执行完),所以直接找'x',但因为在test1中的代码执行不下去,所以报错。执行文件不等于就完全导入文件了。

解决方法1:导入语句放在最后

解决方法2:导入语句放在函数中(因为在导入模块时,函数内的代码并不会执行,只会判断语法错误,所以这时候导入模块可以完全导入)

5 模块的重载

考虑到性能的原因,每个模块只被导入一次,放入字典sys.module中,如果你改变了模块的内容,必须重启程序(python不支持重新加载或卸载之前导入的模块)

就算在修改已经导入的模块里面的代码对运行结果也没影响。

6 py文件区分两种用途:模块与脚本

  1. 脚本,一个文件就是整个程序,用来被执行
  2. 模块,文件中存放着一堆功能,用来被导入使用

python内置了全局变量__name__,

  • 当文件被当做脚本执行时:__name__等于'main'
  • 当文件被当做模块导入时:__name__等于模块名

作用:用来控制.py文件在不同的应用场景下执行不同的逻辑

if __name__ == '__main__':
pass

7 模块搜索路径

模块的查找顺序是:内存中已经加载的模块--》内置模块--》sys.path路径中包含的模块

详细:在第一次导入某个模块式,会先检查该模块是否已经被加载到内存中(当前执行文件的名称空对应的内存),如果有则直接引用;

ps:python解释器会在启动时自动加载一些模块到内存中,可以使用sys.module查看。

如果在内存中没有,解释器会查找同名的内建模块;

如果还没有则去sys.path给出的目录列表中查找。

了解:sys.path的初始化的值来自于:

The directory containing the input script (or the current diretory whrn no files is specified).

PYTHONPATH (a list of directory names, with the same syntax as the shell variable PATH).

The installation-dependent default.

在初始化后,python程序可以修改sys.path,路径放在前面的优于标准库被加载。

搜索时按照sys.path中从左到右的顺序查找,位于前面的优先被查找,sys.path中还可能包含.zip归档文件和.egg文件,python会把.zip归档文件当成一个目录去处理。

.egg文件是setuptools创建的包,这是按照第三方python库和扩展时使用的一种常见格式,.egg文件实际上只是添加了额外元数据(如版本号,依赖项等)的.zip文件。

只能从.zip文件中导入.py,.pyc等文件。使用C编写的共享库和扩展块无法直接从.zip文件中加载(此时setuptools等打包系统有时能提供一种规避方法),且从.zip中加载文件不会创建.pyc或者.pyo文件,因此一定要事先创建他们,来避免加载模块是性能下降。

8 编译python文件

为了提高加载模块的速度,强调强调强调:提高的是加载速度而绝非运行速度。python解释器会在__pycache__目录中下缓存每个模块编译后的版本,格式为:module.version.pyc。通常会包含python的版本号。例如,在CPython3.3版本下,spam.py模块会被缓存成__pycache__/spam.cpython-33.pyc。这种命名规范保证了编译后的结果多版本共存。

Python检查源文件的修改时间与编译的版本进行对比,如果过期就需要重新编译。这是完全自动的过程。并且编译的模块是平*立的,所以相同的库可以在不同的架构的系统之间共享,即pyc使一种跨平台的字节码,类似于JAVA火.NET,是由python虚拟机来执行的,但是pyc的内容跟python的版本相关,不同的版本编译后的pyc文件不同,2.5编译的pyc文件不能到3.5上执行,并且pyc文件是可以反编译的,因而它的出现仅仅是用来提升模块的加载速度的,不是用来加密的。

五 包

1 什么是包?

包是一种通过'.模块名'来组织python模块名称的方式。

具体的:包就是一个包含有__init__.py文件的文件夹,所以其实我们创建包的目的就是为了用文件夹将文件/模块组织起来。

2 为何使用包?

包的本质就是一个文件夹,文件夹的功能就是将同类型的文件组织起来。

其实使用包的原因和使用模块的原因是一样的,都是为了提高程序的结构性和可维护性。

3 注意事项

  1. 关于包相关的导入语句也分为import和from ... import ...两种,但是无论哪种,无论在什么位置,在导入时都必须遵循一个原则:凡是在导入时带点的,点的左边都必须是一个包,否则非法。可以带有一连串的点,但都必须遵循这个原则。但对于导入后,在使用时就没有这种限制了,点的左边可以是包,模块,函数,类(它们都可以用点的方式调用自己的属性)。

  2. import导入文件时,产生名称空间中的名字来源于文件,import 包,产生的名称空间的名字同样来源于文件,即包下的__init__.py,导入包本质就是在导入该文件

  3. 包A和包B下如果有同名模块也不会冲突,因为他们的名称空间是独立的。

4 绝对导入和相对导入

包是写给别人用的,然而在包的内部也会有彼此之间互相导入的需求,这时候就有绝对导入和相对导入两种方式:

绝对导入:以包作为起点

相对导入:以.或..的方式作为起点(只能在一个包中使用,不能用于不同目录内)

**包以及包所包含的模块都是用来被导入的,而不是直接执行的。而环境变量都是以执行文件为准。

5 绝对导入与相对导入总结

绝对导入与相对导入

# 绝对导入: 以执行文件的sys.path为起始点开始导入,称之为绝对导入
# 优点: 执行文件与被导入的模块中都可以使用
# 缺点: 所有导入都是以sys.path为起始点,导入麻烦 # 相对导入: 参照当前所在文件的文件夹为起始开始查找,称之为相对导入
# 符号: .代表当前所在文件的文件加,..代表上一级文件夹,...代表上一级的上一级文件夹
# 优点: 导入更加简单
# 缺点: 只能在导入包中的模块时才能使用
     #注意:
        1. 相对导入只能用于包内部模块之间的相互导入,导入者与被导入者都必须存在于一个包内
        2. attempted relative import beyond top-level package # 试图在*包之外使用相对导入是错误的,言外之意,必须在*包内使用相对导入,每增加一个.代表跳到上一级文件夹,而上一级不应该超出*包