Loading

day 21 模块

1 什么是模块?

  • 模块就是一系列功能的集合体,分为三大类

    I:内置的模块

    II:第三方的模块

    III:自定义的模块

    一个 python 文件本身就一个模块,文件名 m.py,模块名叫 m

  • 模块有四种形式

    1 使用 python 编写的.py 文件

    2 已被编译为共享库或 DLL 的 C 或 C++扩展

    3 把一系列模块组织到一起的文件夹(注:文件夹下有一个__ init __.py 文件,该文件夹称之为包)

    4 使用 C 编写并链接到 python 解释器的内置模块

2 为何用模块

I:内置与第三的模块拿来就用,无需定义,这种拿来主义,可以极大地提升自己的开发效率

II:自定义的模块,可以将程序的各部分功能提取出来放到一模块中为大家共享使用,好处是减少了代码冗余,程序组织结构更加清晰

3 如何用模块

3.1 首次导入模块会发生 3 件事

1 执行 foo.py

2 产生 foo.py 的名称空间,将 foo.py 运行过程中产生的名字都丢到 foo 的名称空间中

3 在当前文件中产生的有一个名字 foo,该名字指向 2 中产生的名称空间,之后的导入,都是直接引用首次导入产生的 foo.py 名称空间,不会重复执行代码# import 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全更新为纯小写形式。

3.2 引用

  • 强调 1:模块名.名字,是指名道姓地问某一个模块要名字对应的值,不会与当前名称空间中的名字发生冲突

    x=1111111111111
    
    print(x)
    
    print(foo.x)
    
  • 强调 2:无论是查看还是修改操作的都是模块本身,与调用位置无关

    import foo
    
    x=3333333333
    
     foo.get()
    
    foo.change()
    
    print(x)
    
    print(foo.x)
    
    foo.get()
    

3.3 可以以逗号为分隔符在一行导入多个模块

  • 建议如下所示导入多个模块,用import语句导入多个模块,可以写多行import语句

    import time
    
    import foo
    
    import m
    
  • 不建议在一行同时导入多个模块

    import time, foo, m
    

3.4 导入模块的规范

  • I. python 内置模块

  • II. 第三方模块

  • III. 程序员自定义模块

import time
import sys

import 第三方 1
import 第三方 2 

import 自定义模块 1
import 自定义模块 2
import 自定义模块 3

3.5 import ... as ...

 import foo as f           # f=foo
 f.get()

 import abcdefgadfadfas
 abcdefgadfadfas.f1
 abcdefgadfadfas.f2
 abcdefgadfadfas.f3

 import abcdefgadfadfas as mmm
 mmm.f1
 mmm.f2
 mmm.f3

3.6 补充

#1、在Python中模块也属于第一类对象,可以进行赋值、以数据形式传递以及作为容器类型的元素等操作。
#2、模块名应该遵循小写形式,标准库从python2过渡到python3做出了很多这类调整,比如ConfigParser、Queue、SocketServer全更新为纯小写形式。
#3、自定义模块的命名应该采用纯小写+下划线的风格 

3.7 可以在函数内导入模块

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

def func():
	import foo

4 模块导入

4.1 一个py文件有几种用途

  • 一个 python 文件有两种用途

    1、被当成程序运行

    2、被当做模块导入

  • 二者的区别是什么?

4.2 模块导入

4.2.1 impot 导入

impot 导入模块在使用时必须加前缀"模块."

优点:肯定不会与当前名称空间中的名字冲突

缺点:加前缀显得麻烦

4.2.2 from-import 语句

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

  • from...impot...导入模块在使用时不用加前缀

    优点:代码更精简

    缺点:容易与当前名称空间混淆

    from ... import ...导入也发生了三件事

1、产一个模块的名称空间
2、运行 foo.py 将运行过程中产生的名字都丢到模块的名称空间去
3、在当前名称空间拿到一个名字,该名字与模块名称空间中的某一个内存地址

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

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

from foo import x。x=模块 foo 中值 x 的内存地址,这里意味着如果foo中x为不可变类型时,当其发生了改变会产生一个新值,这里的x仍然指向旧值,不会同步改变,如果想拿到x的新值,只能更新(重新导入)

​ 同样,一行导入多个名字不推荐

from foo import x,get,change

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

  • *:导入模块中的所有名字

    from foo import * #把foo中所有的名字都导入到当前执行文件的名称空间中,在当前位置直接可以使用这些名字
    
    a=x
    get()
    change()
    obj=Foo()
    

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

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

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) #最终调用的方式是一致的

4.3 循环导入问题

​ 循环导入问题指的是在一个模块加载/导入的过程中导入另外一个模块,而在另外一个模块中又返回来导入第一个模块中的名字,由于第一个模块尚未加载完毕,所以引用失败、抛出异常,究其根源就是在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

测试一

#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中,所以报错

测试二

#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()

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

4.4 模块导入的路径搜索优先级

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

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

2、包含一系列模块的包

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

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

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

优先级:

1、内存(内置模块)

2、硬盘:按照 sys.path 中存放的文件的顺序依次查找要导入的模块

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

>>> sys.path
['',
'/Library/Frameworks/Python.framework/Versions/3.5/lib/python35.zip',
'/Library/Frameworks/Python.framework/Versions/3.5/lib/python3.5',
...,
'/Library/Frameworks/Python.framework/Versions/3.5/lib/python3.5/site-packages'

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

 import sys  
 print(sys.path)  #值为一个列表,存放了一系列的对文件夹,其中第一个文件夹是当前执行文件所在的文件夹

 import foo       # 内存中已经有 foo 了
 foo.say()

 import time
 time.sleep(10)

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在何处,我们都可以导入它了

找 foo.py 就把 foo.py 的文件夹添加到环境变量中

import sys
sys.path.append(r'/Users/linhaifeng/PycharmProjects/s14/day21/aa')

import foo
foo.say()
from foo import say

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

 import sys
 import foo    # foo=模块的内存地址
 
 del foo
 	def func():
		 import foo # foo=模块的内存地址

 func()
 print('foo' in sys.modules)
 print(sys.modules)

4.5 区分py文件的两种用途

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

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

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

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

posted @ 2021-12-05 10:47  maju  阅读(32)  评论(0)    收藏  举报