Python基础08-模块与包
模块
当程序较为复杂时,我们可以将不同类型的功能拆分成不同的模块,每个模块建立一个.py脚本。这样做的好处是:
- 不同模块负责不同部分的内容,逻辑更清晰;
- 公共模块部分可以复用;
- 模块拥有独立的命名空间(不同模块中可以拥有同名的变量/函数/类型)。
在Python中,一个.py脚本就是一个模块
模块导入
一个模块可以导入其他模块,常用的导入方式如下:
import moudle
如import time
,此时模块名被导入到当前命名空间,使用模块名引用其中的函数,如time.sleep(1)
。
注意:使用import 语句时,最小导入到模块,不能直接使用import 导入模块中的变量/函数/类,如
import time.sleep
是错误的,如果要导入模块中的变量/函数/类,要使用from .. import ...
语句。
from moudle import ...
导入模块部分变量/函数/类
如from time import localtime,sleep
,此时localtime
,和sleep
被导入到当前命名空间中,直接使用导入的函数/类名称进行操作,如sleep(1)
,注意,由于模块名未被导入,不能使用time.sleep(1)
;
导入同一模块多个变量/函数/类时,可以分开导入,也可以一起导入,一下三种方式均可:
# 分开导入
from time import localtime
from time import sleep
# 一起导入
from time import localtime, sleep
# 一起导入
from time import (localtime,
sleep)
相比于部分导入,使用
sleep(1)
, 导入模块并使用模块名引用,time.sleep(1)
,函数来源显得更清晰。
from moudle import *
如,from time import *
,将导入time
模块所有,非下划线_
开头的变量/函数/类。
同时被导入模块中可以使用__all__来设置from ... import *
时允许被导入到全部变量/函数/类,示例如下:
b.py内容
__all__ = ['b', 'c'] # 设置from b import * 时只导入b和c变量
b = 1
_b = 2 # 下划线开头到变量视为私有变量,from b import * 时不会被导入
c = 3
d = 4
a.py内容
from b import *
c = 4 # 将覆盖b中导入的c
print(b)
print(c)
# print(d) # 由于没有导入d,因此打印d会报错
如果想使用b模块的私有变量_b,可以使用
import b; print(b._b)
,或from b import _b; print(_b)
等方式。
这种方式是不被推荐的,原因如下:
- 不知道都导入了哪些变量/模块/类;
- 把其他模块的变量全部导入到当前命名空间,可能导致名称覆盖(重复),及来源不清楚。
导入并设置别名
导入时为了使用简单或者避免命名冲突,可以设置别名,如:
from time import sleep as wait # 设置别名为wait
wait(1)
也可以使用import_lib根据模块名字符串进行导入,例如
from importlib import import_module; time = import_module('time')
模块导入常见问题
主模块和导入模块
模块分为主模块和导入模块,当前执行模块被称为主模块,其他被导入的模块称为导入模块。可以使用Python魔术变量__name__
(注意不加引号),来查看当前模块是否主模块。
如果当前模块为主模块时,模块名__name__
为字符串'__main__'
,否则为其脚本名去掉.py。例如:
b.py
print('b模块,当前模块名', __name__)
a.py
import b
print('a模块,当前模块名', __name__)
运行后结果显示:
b模块,当前模块名 b
a模块,当前模块名 __main__
因此在模块中常用if __name__ == '__main__'
来判断当前模块是不是主模块(即是不是以当前脚本为开始运行的),如:
b.py
print('b模块,当前模块名', __name__)
if __name__ == '__main__':
print('模块私有代码,只有b模块自己运行才会输出,别的模块导入不会输出')
运行a.py不会输出b模块中,模块私有代码的打印,只有运行b.py时才会输出,因此也被称为模块私有代码,并常用于模块自测代码。
模块导入路径
当从一个模块导入另一个模块时,查找路径为:
- 运行脚本所在目录
- 系统环境变量PYTHONPATH设置的目录
- Python三方包site-packages中查找
如果找不到将抛出ModuleNotFoundError
,找不到模块异常。
间接导入问题
间接导入是指a模块导入b模块,b模块导入c模块,运行a模块时c模块被间接导入。
间接导入时,需要注意模块的相对位置问题。假设b模块和c模块在同一dir目录,a模块在外层目录,目录结构如下:
dir/
b.py
c.py
a.py
c.py内容
print('我是c模块')
b.py内容
import c
print('我是b模块')
a.py内容
from dir import b
print('我是主模块')
此时,运行a.py,间接导入c模块时便会出现异常
...
import c
ModuleNotFoundError: No module named 'c'
原因为,运行a.py时,间接导入import c
时,只会在a.py所在目录(当前运行目录)及PYTHONPATH、三方包site-packages中查找。而不会切换目录到b.py所在目录进行查找。
(相当于只把导入语句原样拿过来,但是并没有切换目录),这就导致找不到c模块。
处理方式有以下两种:
- 将dir目录加入到PYTHONPATH中
修改b.py内容
import os
import sys
dir_path = os.path.dirname(__file__) # 获取当前文件所在目录,即dir目录路径
sys.path.append(dir_path) # 将dir目录路径加入到模块查找路径PYTHONPATH中
# sys.path.insert(0, dir_path) # 也可以插入到最前面
import c
print('我是b模块')
- 所有导入从项目根目录开始导入
修改b.py中导入c模块语句为从项目根目录导入,并把项目根目录导入PYTHONPATH中
from dir import c
print('我是b模块')
注意:如果是在PyCharm中新建的项目,PyCharm会自动将项目根目录加入到PYTHONPATH中,因此可以直接运行b.py。
或者可以在命令行,在系统根目录运行python3 dir/b.py
,也可以正常导入c模块。
注意:此时如果
cd dir ; python3 b.py
仍会报错,因为导入时基于项目根目录到,在dir目录运行脚本,无法确保系统根目录被导入。
循环导入问题
循环导入指a导入了b模块,b模块又直接或间接导入了a模块。循环导入不一定会出现问题:
- 使用
import 模块
导入时,允许循环导入,模块只在第一次导入时及作为主模块时运行 - 使用
from ... import ...
时不允许循环导入,
例如:
c.py文件内容
import b
var_a = 1
print('我是c模块')
b.py文件内容
import c
var_b = 2
print('我是b模块')
a.py文件内容
import a
var_c = 3
print('我是a模块')
运行a.py模块,输出
我是a模块
我是c模块
我是b模块
我是a模块
a模块导入b模块,间接导入c模块,又间接导入自己时运行一次,a模块作为主模块运行一次。
如果改为from ... import ...
方式,例如:
c.py文件内容
from a import var_a
var_c = 3
print('我是c模块')
b.py文件内容
from c import var_c
var_b = 2
print('我是b模块')
a.py文件内容
from b import var_b
var_a = 1
print('我是a模块')
此时运行a模块则会报错:
ImportError: cannot import name 'var_b' from partially initialized module 'b' (most likely due to a circular import) ...
处理方法有以下几种:
- 使用
import 模块
方式导入 - 将
from ... import ...
改到函数内部(延迟执行) - 使用包
__init__.py
统一规划导入方式
导入立即执行及调用(延迟)执行
在导入模块时,有些是立即执行,有些时调用时才执行的。
第一次导入模块时立即执行的有:
- 直接写模块中的语句
- 装饰器
- 类变量
调用时才执行的有:
- 定义的函数
- 定义的类及方法
示例如下:
b.py内容
def deco(func): # 装饰器函数
print('调用 %s' % func.__name__)
return func
@deco # 装饰器调用,第一次导入模块时立即执行
def add(x, y): # 函数定义,调用函数时执行
print('%s+%s' % (x, y))
return x + y
s1 = add(1, 2) # 模块语句,第一次导入模块时立即执行
class Calc: # 类定义,调用类(生成对象时) 执行__init__方法
s2 = add(3, 4) # 类属性,第一次导入模块时立即执行
def __init__(self):
print('对象初始化')
def sub(self, x, y): # 对象方法,调用时执行
print('%s-%s' % (x, y))
return x - y
a.py内容
import b
执行a.py,显示内容如下:
调用 add
1+2
3+4
包Package
当拥有多个模块时,我们可以使用包来组织同一类别或层次的模块。在Python
在Python中,一个目录被视为包,包中可以包含其他子包(子目录),包中不强制必须有__init__.py
文件。
包中可以包含__init__.py
文件作为包的初始化配置,__init__.py
可以为空。
在Python3中,目录中是否包含__init__.py
机会没有区别,我们可以使用Python魔术变量__pacakge__查看当前模块所在包路径。
Python中常用的魔术变量如下:
__file__
:表示当前脚本路径__name__
: 表示当前模块导入路径,作为主模块时(当前运行脚本),其值为'__main__'
,否则为其模块导入路径__package__
: 当前包导入路径,作为主模块时(当前运行脚本),其值为None
,否则为其模块所在包导入路径__buildins__
: 当前所有可用内置变量/函数组成的字典
导入包及模块
同模块导入一样,包的导入也支持import ...
和from ... import ...
两种导入方式。
假设项目结构如下:
package/
sub_package/
__init__.py
c.py
__init__.py
a.py
b.py
main.py
package/sub_package/c.py内容
var_c = 3
print('我是c模块 模块导入路径 %s 包路径 %s' % (__name__, __package__))
package/sub_package/init.py内容
var_sub_package = 5
packcage/init.py内容
var_package = 4
package/b.py内容
var_b = 2
print('我是b模块 模块导入路径 %s 包路径 %s' % (__name__, __package__))
package/a.py内容
var_a = 1
print('我是a模块 模块导入路径 %s 包路径 %s' % (__name__, __package__))
导入包及模块的常用方式如下。
导入包或子包
例如在main.py中,可以使用如下语句导入包或子包
- import package
- import package.sub_package
此时实际导入的是package/__init__.py
,及package/sub_package/__init__.py
,例如:
import package
print(package.var_package)
# 不能使用package.a 引用模块
或
import package.sub_package
print(package.sub_package.var_sub_package)
# 由于导入的不是package,不能使用package.var_package
# 不能使用package.sub_package.c 引用模块
注意:导入为package.sub_package,则需要使用
package.sub_package
来引用sub_pages/__init__.py
中的变量,不能直接使用package。
直接导入包或子包是,如果对于__init__.py中没有对应的变量,不能使用package或package.sub
全部导入
例如在main.py中,可以使用如下语句导入包或子包模块
import package.a, package.b
import package.sub_package.c
此时,可以使用导入的模块引用其中的变量/函数/类,使用的名称要和import后面的名称一致,如:
main.py内容
import package.a, package.b
import package.sub_package.c
print(package.a.var_a)
print(package.b.var_b)
print(package.sub_package.c.var_c)
注意:使用
import 包.子包.模块
时,最小导入到模块,不能使用import语句直接导入模块中的变量/函数/类。
部分导入
from ... import ...
方式支持从包导入子包,从包.子包导入模块,从包.子包.模块导入变量/函数/类等,如
main.py内容
# 从包导入子包(实际为子包__init__.py)
from package import sub_package
print(sub_package.var_sub_package) # 使用名称和import后面的名称要一致
# 从包.子包导入子包`__init__.py`中的变量/函数/类
from package.sub_package import var_sub_package
print(var_sub_package) # 使用名称和import后面的名称要一致
# 从包 或 包.子包中导入模块
from package import a
from package.sub_package import c
print(a.var_a) # 使用名称和import后面的名称要一致
print(c.var_c) # 使用名称和import后面的名称要一致
# 从包.模块 或 包.子包.模块中导入变量/函数/类
from package.a import var_a
from package.sub_package.c import var_c
print(var_a) # 使用名称和import后面的名称要一致
print(var_c) # 使用名称和import后面的名称要一致
注意:无论使用
import ...
还是from ... import ...
,使用名称要和import后面导入到当前命名空间的变量名一致
包中的相对导入
包(目录中)可以使用相对路径导入同级路径或上级路径的模块,如
package/sub_package/c.py内容
from ..b import var_b # 使用相对导入,导入上级包中的b模块
var_c = 3
print('我是c模块 模块导入路径 %s 包路径 %s' % (__name__, __package__))
package/b.py内容
var_b = 2
print('我是b模块 模块导入路径 %s 包路径 %s' % (__name__, __package__))
package/a.py内容
from .b import var_b # 使用相对导入,同级包中的b模块
from .sub_package.c import var_c # 使用相对导入,同级包sub_package子包中的c模块
var_a = 1
print('我是a模块 模块导入路径 %s 包路径 %s' % (__name__, __package__))
main.py内容
from package import a # 导入a模块
运行main.py,显示结果如下:
我是b模块 模块导入路径 package.b 包路径 package
我是c模块 模块导入路径 package.sub_package.c 包路径 package.sub_package
我是a模块 模块导入路径 package.a 包路径 package
注意⚠️:模块作为主模块(自己运行)及导入模块(导入运行)的逻辑是非常不一样的。例如,上例中如果直接运行a.py,a模块作为主模块时其
__package__
属性为None,从而不支持相对导入。
因此如果想模块即能单独执行(作为主模块执行),可支持导入执行,则要慎用相对导入。
包中__init__.py
的作用
包中的__init__.py
常用作包的初始配置,常见使用场景如下:
- 导入包时进行一些初始化操作
- 修改包中模块的导入设置
从包中导入模块时的查找顺序为,先从包__init__.py
中查找同名变量,如果没有,则在包所在目录中查找同名脚本(模块)。
例如:
在package/__init__.py
中添加变量a,内容如下:
a = '变量a'
在main.py中通过package包导入a时,实际导入的事__init__.py
中的变量a,而不是a模块
main.py内容
from package import a
print(a)
一般可以使用__init__.py来整理包所要暴露的模块及功能(模块中的变量/函数/类)
例如修改package/__init__.py
代码如下:
from . import a
from . import b
from .sub_package import c
from .a import var_a
from .b import var_b
from .sub_package.c import var_c
此时我们便可以直接通过package导入所暴露的变量,例如在main.py中
# 通过模块直接导入
from package import a, b, c, var_a, var_b, var_c
print(a.var_a, b.var_b, c.var_c)
print(var_a, var_b, var_c)
原来,导入c模块需要使用from package.sub_package import c
,导入var_c则需要使用from package.sub_package.c import var_c
,现在则可以直接导入。
同时,在包__init__.py
中也可以下划线开头的私有变量,及使用__all__
变量来限制from package import *
时导入的全部变量。