python高级之模块

模块

image

什么是模块

模块就是一系列功能的集合体,分为三大类:
1.内置的模块
2.第三方的模块
3.自定义的模块
一个python文件本身就是一个模块,文件名叫m.py,模块名叫m
模块分为四种形式:
1.使用python编写的.py文件
2.已经被编译为共享库或DLL的c或C++扩展
3.把一系列模块组织到一起的文件夹(注:文件夹下有一个__init__.py文件,该文件称之为包)
4.使用C编写并链接到python解释器的内置模块
24为了解内容

为何要用模块

1.内置与第三方的模块拿来就用,无需定义,这种拿来主义,可以极大的提升自己发效率
2.自定义模块可以将程序的各部分功能提取出来放到一模块中为大家共享使用,好处是减少了代码冗余,程序组织结构更加清晰

如何使用模块

1、一个py文件有几种用途?

1、被当成程序运行
2、被当成模块导入
__name__:
1、当foo.py被运行时,__name__的值为'__main__'
2、当foo.py被当作模块导入时,__name__的值为'foo'

2、import

准备

  • 新建一个文件 foo.py
#文件名: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()

首次导入模块会发生三件事

1.执行foo.py
2.产生foo.py的名称空间,将foo.py运行过程中产生的名字都丢到foo的名称空间中
3.在当前文件中产生的有一个名字foo,该名字指向2中产生的名称空间
之后的导入都是直接引用首次导入产生的foo.py的名称空间,不会重复执行代码

导入模块的规范

按照这个顺序来进行:
1.python内置模块
2.第三方模块
3.程序员自定义模块
例如:
import time
import sys
import 第三方1
import 第三方2
import 自定义模块1
import 自定义模块2
需要注重的一些细节;
1.import ... as ...(当你的模块名比较长的时候可以选择起别名来使用该模块)
import foo as f # f = foo
2.模块是第一类对象
3.自定义模块应该采用纯小写加下划线的风格
4.可以在函数内部导入模块,但该模块只能在该函数内使用
def func():
import foo
强调1:模块名.名字,是指名道姓的问某一个模块要名字对应的值,不会与当前名称空间发生冲突
强调2:无论是查看还是修改都是以原模块为基准的,与调用位置无关
强调3:可以以逗号为分隔符在一行导入多个模块,虽然可以这么写,但是不建议在一行同时导入多个模块,也不建议大多数情况下使用from ... import *,不然容易导致变量修改但没报错

3、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()

__all__变量

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

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

tset1:

#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'
#2、分析
先执行run.py--->执行import m1,开始导入m1并运行其内部代码--->打印内容"正在导入m1"
--->执行from m2 import y 开始导入m2并运行其内部代码--->打印内容“正在导入m2”--->执行from m1 import x,由于m1已经被导入过了,所以不会重新导入,所以直接去m1中拿x,然而x此时并没有存在于m1中,所以报错

test2:

#1、执行文件不等于导入文件,比如执行m1.py不等于导入了m1
直接执行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'
#2、分析
执行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中所以报错

解决方案

# 方案一:导入语句放到最后,保证在导入时,所有名字都已经加载过
# 文件: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()

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

5、搜索模块的路径与优先级

模块其实分为四个通用类别,分别是:

1、使用纯Python代码编写的py文件

2、包含一系列模块的包

3、使用C编写并链接到Python解释器中的内置模块

4、使用C或C++编译的扩展模块

优先级:

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

import sys
print(sys.path)
['E:\\python\\PyCharm 2023.2.1\\workspace\\ATM', 'E:\\python\\PyCharm 2023.2.1\\workspace\\ATM', 'E:\\python\\PyCharm 2023.2.1\\plugins\\python\\helpers\\pycharm_display', 'E:\\python\\python310\\python310.zip', 'E:\\python\\python310\\DLLs', 'E:\\python\\python310\\lib', 'E:\\python\\python310', 'E:\\python\\python310\\lib\\site-packages', 'E:\\python\\PyCharm 2023.2.1\\plugins\\python\\helpers\\pycharm_matplotlib_backend']

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

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

# foo.py与本模块不在同一个文件夹的时候使用
import sys
sys.path.append(r'/pythoner/projects/') #也可以使用sys.path.insert(……)
import foo #无论foo.py在何处,我们都可以导入它了

了解:

import sys
print(sys.modules) ## 查看已经加载到内存里的模块
环境变量是以执行文件为准备的,所有的被导入的模块或者说后续的其他文件引用的sys.pathd

6、区分py文件的两种用途

一个Python文件有两种用途,一种被当主程序/脚本执行,另一种被当模块导入,为了区别同一个文件的不同用途,每个py文件都内置了__name__变量,该变量在py文件被当做脚本执行时赋值为“main”,在py文件被当做模块导入时赋值为模块名

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

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

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

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

类型提示 Type hinting(最低Python版本为3.5)

python3新增类型提示功能,例如我们可以为函数增加类型提示信息,而不影响函数本身的执行:

注释的一般规则是参数名后跟一个冒号(:),然后再跟一个expression,这个expression可以是任何形式。

def func(a: 'spam', b: (1, 10), c: float) -> int:
return a + b + c
func(1, 2, 3)
6

返回值的形式是 -> int,annotation可被保存为函数的attributes。查看所有的annotation,可通过如下语句

func.__annotations__
{'c': <class 'float'>, 'a': 'spam', 'b': (1, 10), 'return': <class 'int'>}

如果为函数增加了注释,可不可以继续使用默认参数呢?答案是肯定的。

def func(a: 'spam' = 4, b: (1, 10) = 5, c: float = 6) -> int:
... return a + b + c
...
func(1, 2, 3) # 6
func() # 15
func(1, c=10) # 16
func.__annotations__
{'c': <class 'float'>, 'a': 'spam', 'b': (1, 10), 'return': <class 'int'>}
posted @   Xiao0101  阅读(12)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· 单线程的Redis速度为什么快?
点击右上角即可分享
微信分享提示

目录