模块导入与包
注意噢!! global关键字可以声明全局变量, 但仅限于一个文件中(其他文件可以通过import导入,但修改并不影响原始值)
# 模块
参考链接:
https://zhuanlan.zhihu.com/p/109127048
# 模块导入
# 导入模块的规范
# -- 1. python内置模块
# -- 2. 第三方模块
# -- 3. 程序员自定义模块
2
3
Ps: 在文件开头导入模块属于全局作用域, 在函数内导入的模块则属于局部的作用域
# import语句
# -- 文件名:foo.py
__all__=['x','get'] # -- 该列表中所有的元素必须是字符串类型,每个元素对应foo.py中的一个名字
# __all__ 用于控制 `from foo import *` 语句中 * 代表的意思
x=1
def get():
print(x)
def change():
global x
x=0
class Foo:
def func(self):
print('from the func')
2
3
4
5
6
7
8
9
10
11
12
想在run.py文件中引用foo.py中的功能, 需要使用 import foo
, 首次导入模块会做三件事:
1> 执行源文件foo.py代码
2> 产生一个新的名称空间用于存放源文件执行过程中产生的名字
3> 在当前执行文件run.py所在的全局名称空间中得到一个名字foo, 该名字指向新创建的模块名称空间
若要引用模块名称空间中的名字,需要加上该前缀
# -- 文件名:run.py
import foo
x = 5
a = foo.x # -- 引用模块foo中变量x的值赋值给当前名称空间中的名字a
foo.get() # -- 调用模块foo的get函数
foo.change() # -- 调用模块foo中的change函数
obj = foo.Foo() # -- 使用模块foo的类Foo来实例化,进一步可以执行obj.func()
2
3
4
5
6
7
注意: 加上 foo.
作为前缀就相当于指名道姓地说明要引用foo名称空间中的名字, 所以肯定不会与当前执行文件所在名称空间中的名字相冲突, 并且若当前执行文件的名称空间中存在x, 执行 foo.get()
或 foo.change()
操作的都是 源文件中的 全局变量x
# from-import语句
from..import..与import语句基本一致,也会有那三个步骤 唯一不同的是:
使用import foo
导入模块后, 引用模块中的名字都需要加上foo.作为前缀;
而使用 from foo import x,get,change,Foo
则可以在当前执行文件中直接引用模块foo中的名字.
# -- 文件名:run.py
from foo import x,get,change # -- 将模块foo中的x和get导入到当前执行文件run.py的全局名称空间
a = x # -- 直接使用模块foo中的x赋值给a
get() # -- 直接执行foo中的get函数
change() # -- 即便是当前有重名的x,修改的仍然是源文件foo.py中的x
"""
思考了下,change()后,run.py中a变量的值是否一同改变,取决于foo.py中的x变量是可变类型,还是不可变类型.
下面 "重复导入" 的部分会包含这部分思考的分析.
"""
from foo import * # -- 把foo中所有的名字都导入到当前执行文件的名称空间中
若我们需要引用模块中的名字过多的话,可以采用上述的导入形式来达到节省代码量的效果.
这会导致我们无法搞清楚究竟从源文件中导入了哪些名字到当前位置,这极有可能与当前位置的名字产生冲突
需要强调的是: 只能在模块最顶层使用的方式导入,在函数内则非法..
模块的编写者可以在自己的文件中定义__all__变量用来控制*代表的意思
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
无需加前缀的好处是使得我们的代码更加简洁, 坏处则是容易与当前名称空间中的名字冲突. 如果当前名称空间存在相同的名字, 则后定义的名字会覆盖之前定义的名字.
# 重复导入
强调: 第一次导入模块已经将其加载到内存空间了,之后的重复导入( 不管是当前可执行文件的重复导入,还是多个文件导入同一个模块 )会直接引用内存中已存在的模块(已经存在的命名空间),不会重复执行导入的模块
"""执行run.py 结果如下:
10
[]
12
[1]
10
[1]
这样分析:
scope(run.py) scope(a.py) scope(b.py) heap堆区
x:Ox101 x:Ox101 --> x:Ox301 a Ox101 10
y:Ox201 y:Ox201 Ox201 [] --> [1]
change_x Ox301 12
change_y id(change_x)
id(change_y)
记住一个原则即可:
`from a import x,y` 方式导入进scope(run.py)的x,y
x是不可变变量,源文件a.py中的x改变了,run.py中的x不会改变,因为引用计数不为0
y是可变变量,其值可以原地改变.需要注意哈!
`import a` 方式导入,要访问scope(a.py)中的名字 是需要加前缀的!
scope(a.py)中的x值变了,a.x方式访问,结果肯定也会变啦.
"""
# -- a.py
x = 10
y = []
def change_x():
global x
x = 12
def change_y():
y.append(1)
# -- run.py
from a import x, y
print(x) # 10
print(y) # []
import b
print(x) # 10
print(y) # [1]
# -- b.py
import a
a.change_x()
a.change_y()
print(a.x) # 12
print(a.y) # [1]
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
# 其它导入语法(as)
import foo as f # -- 为导入的模块foo在当前位置起别名f
from foo import get as get_x # -- 还可以为导入的某个名字起别名
2
为关键字import后导入的名字起别名,有三大好处
1> 被导入的名字过长时采用起别名的方式来精简代码;
2> 为被导入的名字起别名可以很好地避免与当前名字发生冲突;
3> 还可以保持调用方式的一致性
"""
我们有两个模块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
3
4
5
6
7
8
9
10
# 循环导入问题
循环导入问题指的是在一个模块加载/导入的过程中导入另外一个模块, 而在另外一个模块中又返回来导入第一个模块中的名字, 由于第一个模块尚未加载完毕 , 所以引用失败、抛出异常.
究其根源 就是在python中, 同一个模块只会在第一次导入时执行其内部代码, 再次导入该模块时, 即便是该模块尚未完全加载完毕也不会去重复执行内部代码..
# -- m1.py
print('正在执行m1.py文件')
from m2 import y
x='m1'
# -- m2.py
print('正在执行m2.py文件')
from m1 import x
y='m2'
# -- run.py
import m1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 测试一
★ 运行run.py
1> `import m1` run.py导入模块m1,开始执行m1.py中的内部代码
2> `from m2 import y` m1模块中导入了m2模块,开始执行m2.py中的内部代码
3> `from m1 import x` m2模块中又导入了还未来得及加载完的m1模块
(注意:此处m1模块被重复导入了,不会重新执行m1.py内部代码)
并引用scope(m1.py)还没来得及加载的x名字,报错.
2
3
4
5
6
# 测试二
强调: 执行文件不等于导入文件, 比如执行m1.py不等于导入了m1
即 执行文件开辟的空间跟首次导入该模块开辟的空间不是同一个空间!
★ 运行m1.py
1> `from m2 import y` m1模块中导入了m2模块,开始执行m2.py中的内部代码
2> `from m1 import x` m2模块中导入了m1模块,开始执行m1.py中的内部代码
3> `from m2 import y` (注意:此处m2模块被重复导入了,不会重新执行m2.py内部代码)
So, m1模块中又导入了还未来得及加载完的m2模块
并引用scope(m2.py)还没来得及加载的y名字,报错.
"""
正在执行m1.py文件
正在执行m2.py文件
正在执行m1.py文件
cannot import name 'y' from 'm2'
"""
2
3
4
5
6
7
8
9
10
11
12
# 解决方案
方案一: 导入语句放到最后, 保证在导入时, 所有名字都已经加载过
方案二: 导入语句放到函数中, 只有在调用函数时才会执行其内部代码
循环导入问题大多数情况是因为程序设计失误导致, 上述解决方案也只是在烂设计之上的无奈之举,在我们的程序中应该尽量避免出现循环/嵌套导入!!
如果多个模块确实都需要共享某些数据, 可以将共享的数据集中存放到某一个地方,然后进行导入
# 模块的查找顺序
模块分为四个通用类别:
1> 使用纯Python代码编写的py文件
2> 包含一系列模块的包
3> 使用C编写并链接到Python解释器中的内置模块
4> 使用C或C++编译的扩展模块
搜索模块的路径与优先级:
○ 在导入一个模块时, 若该模块已加载到内存中, 则直接引用;
○ 否则会优先查找内置模块;
○ 然后按照从左到右的顺序依次检索sys.path中定义的路径, 直到找模块对应的文件为止, 否则抛出异常.
sys.path
也被称为 模块的搜索路径 , 它是一个列表类型
"""
sys.path中的第一个路径通常为空,代表执行文件所在的路径
列表中的每个元素都可以当作一个目录来看:
在列表中会发现有.zip或.egg结尾的文件,二者是不同形式的压缩文件
Python支持从一个压缩文件中导入模块
"""
>>> import sys
>>> sys.path
['', '/Users/One_Piece/Documents', '/Library/Frameworks/Python.framework/Versions/3.8/lib/python38.zip', '/Library/Frameworks/Python.framework/Versions/3.8/lib/python3.8', '/Library/Frameworks/Python.framework/Versions/3.8/lib/python3.8/lib-dynload', '/Users/One_Piece/Library/Python/3.8/lib/python/site-packages', '/Library/Frameworks/Python.framework/Versions/3.8/lib/python3.8/site-packages']
2
3
4
5
6
7
8
9
sys.path中的第一个路径通常为空, 代表执行文件所在的路径.
1> 在被导入模块与执行文件在同一目录下时肯定是可以正常导入的;
2> 针对被导入的模块与执行文件在不同路径下的情况, 为了确保模块对应的源文件仍可以被找到.
需要将源文件foo.py所在的路径添加到sys.path中, 假设foo.py所在的路径为/pythoner/projects/
import sys
# -- 也可以使用sys.path.insert()
sys.path.append(r'/pythoner/projects/')
import foo
2
3
4
5
# PY文件的用途
1> 被当主程序/脚本执行 __name__
被赋值为 __main__
2> 被当模块导入 __name__
被赋值为模块名
# -- foo.py
# 略 略 略
# -- foo.py被当做模块导入时运行的代码,不会执行测试代码
if __name__ == '__main__':
# -- foo.py被当做脚本执行时运行的测试代码
2
3
4
5
6
# 包
参考链接:
https://zhuanlan.zhihu.com/p/109221768
# 包是什么?
随着模块数目的增多, 把所有模块不加区分地放到一起也是极不合理的.创建一个包可以把模块组织到一起.
包就是一个含有__init__.py
文件的文件夹, 文件夹内可以组织 子模块 或 子包 !
- aaa # -- 顶级包
- __init__.py
- m1.py # -- 子模块
- m2.py
- bbb # -- 子包
- __init__.py
- m3.py
- run.py
2
3
4
5
6
7
8
强调:
1> py3中, 即使包下没有 __init__.py
文件, import 包
也不会报错;
而在py2中, 包下 一定 要有该文件, 否则import包报错!!
2> 记住, 包属于模块的一种, 因而包以及包内的模块均是用来被导入使用的, 而绝非被直接执行!!
# 包的使用
首次导入包 如 import pool
同样会做三件事:
1> 执行包下的 __init__.py
文件
2> 产生一个新的名称空间用于存放 __init__.py
执行过程中产生的名字
3> 在当前执行文件所在的名称空间中得到一个名字pool, 该名字指向 __init__.py
的名称空间
导包就是在导入包下的 __init__.py
文件
★ 我们可以使用包将aaa.py文件中的三个函数放到不同的文件中.而且不影响函数原本的调用方式.
"""
# -- aaa.py
def func1():pass
def func2():pass
def func3():pass
"""
- aaa # -- 顶级包
- __init__.py
- m1.py # -- 子模块
- m2.py
- bbb # -- 子包
- __init__.py
- m3.py
- run.py
2
3
4
5
6
7
8
9
10
11
12
13
14
15
绝对导入: 一般以顶级包为起始 (往往会将顶级包加入sys.path路径中)
相对导入: 开始位置的 .
代表当前文件所在的目录, ..
代表当前目录的上一级目录, 以此类推!
针对包内部模块之间的相互导入推荐使用相对导入,需要特别强调
1> 相对导入只能在包内部使用, 用相对导入不同目录下的模块是非法的,不能出包!
2> 无论是import还是from-import.
除相对导入开始位置的点,导入语句中其它位置的点代表的是路径分隔符,并且.
左侧必须是一个包!
3> 使用相对导入的python文件不能作为直接执行的文件!!
★ 包里各个文件的代码如下
把包内部很深的一些类、函数,给外部使用,只需在__init__.py
里 注册/导入 一下!!
不在__init__.py
里注册的,本意是给包内部用,如果外部想用,使用完整路径导入使用!
# -- m1.py
def func1():pass
# -- m2.py
def func2():pass
# -- m3.py
def func3():pass
# -- m4.py
def func4():pass
# -- aaa/__init__.py
# -- 把包内部很深的一些类、函数,给外部使用,只需在__init__.py里 注册/导入 一下!!
# -- 不在__init__.py里注册的,本意是给包内部用,如果外部想用,使用完整路径导入使用!
__all__=['func1','func2','func3','bbb'] # -- 用于限制`from aaa import *`中的*号
from aaa.m1 import func1 # -- 绝对导入 等同于from .m1 import func1
from .m2 import func2 # -- 相对导入
from .bbb.m3 import func3 # -- 等同于 from aaa.bbb.m3 import func3
from . import bbb # -- 拿到aaa.bbb
# bbb/__init__.py
x = 1
# -- run.py
# -- 不管是`import ..`,还是`from .. import ..`导入过程中都会依次执行包下的__init__.py
import aaa
aaa.func1() # -- 来自aaa.__init__
aaa.func2() # -- 来自aaa.__init__
aaa.func3() # -- 来自aaa.__init__
print(aaa.bbb) # -- 来自aaa.__init__
print(aaa.bbb.x) # -- 来自bbb.__init__
# -- 分析下该导入过程,执行包aaa的__init__.py,用到了bbb
# 再执行包bbb下的__init__.py,里面并没有m3,所以就会去sys.path路径里面找
# 注意:包是模块的一种,所以也是要遵循模块查找顺序的!!!
from aaa.bbb.m3 import func3
func3()
"""
scope(包aaa) scope(包bbb) scope(run.py)
func1 func4 aaa
func2 x func3
func3
bbb
"""
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
# 软件开发目录规范
- conf # -- 存放配置文件
- settings.py
- lib # -- 库(模块) 存放程序中常用的自定义模块,里面通常是一些共享的功能
- common.py
- core # -- 存放核心业务逻辑(展示给用户看的东西)
- src.py
- api # -- 存放接口文件,接口主要用于为业务逻辑提供数据操作
- api.py
- db # -- 存放操作数据库相关文件,主要用于与数据库交互
- db.txt
- log # -- 存放程序的日志信息
- access.log
- run.py # -- 程序的启动文件,一般放在项目的根目录下
# 因为在运行时会默认将运行文件所在的文件夹作为sys.path的第一个路径
# 这样就省去了处理环境变量的步骤
- setup.py # -- 安装、部署、打包的脚本
- README # -- 项目说明文件
- requirements.txt # -- 存放软件依赖的外部Python包列表
"""
- bin # -- 存放可执行文件
- run.py # -- 整个程序的入口文件
"""
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
setup.py
一般来说,用setup.py来管理代码的打包、安装、部署问题.
业界标准的写法是用Python流行的打包工具setuptools来管理这些事情,这种方式普遍应用于开源项目中.
不过这里的核心思想不是用标准化的工具来解决这些问题,而是说,一个项目一定要有一个安装部署工具.
能快速便捷的在一台新机器上将环境装好、代码部署好和将程序运行起来.
requirements.txt
requirements.txt文件的存在是为了方便开发者维护软件的依赖库.
我们需要将开发过程中依赖库的信息添加进该文件中,避免在 setup.py安装依赖时漏掉软件包.
同时也方便了使用者明确项目引用了哪些Python包.
这个文件的格式是每一行包含一个包依赖的说明,通常是flask>=0.10这种格式,要求是这个格式能被pip识别.
这样就可以简单的通过pip install -r requirements.txt
来把所有Python依赖库都装好了!
# -- run.py
import sys,os
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
sys.path.append(BASE_DIR)
from core import src
if __name__ == '__main__':
src.run()
"""
__file__ 当前文件所在路径
os.path.abspath(__file__) 当前文件的绝对路径
os.path.dirname() 获取当前文件所在的文件夹路径
"""
2
3
4
5
6
7
8
9
10
11
12
13
# settings.py
import os
BASE_DIR=os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
DB_PATH=os.path.join(BASE_DIR,'db','db.json')
LOG_PATH=os.path.join(BASE_DIR,'log','access.log')
LOGIN_TIMEOUT=5
2
3
4
5
6
7