...

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模块。
处理方式有以下两种:

  1. 将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模块')
  1. 所有导入从项目根目录开始导入
    修改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 *时导入的全部变量。

参考:Python3官方教程-模块

posted @ 2020-04-21 19:32  韩志超  阅读(749)  评论(0编辑  收藏  举报