模块化

一般来说,编程语言中,库、包、模块是同一种概念,是代码组织方式。
Python中只有一种模块对象类型,但是为了模块化组织模块的便利,提供了一个概念一一包模块module,指的是Python的源代码文件
包package,指的是模块组织在一起的和包名同名的目录及具相关文件

导入语句

import语句

语句

含义

import模块1[,模块2,…]

完全导入

import … as …

模块别名

import 语句
1. 找到指定的模块,加载和初始化它,生成模块对象。找不到,抛出ImportError异常
2. 在import所在的作用域的局部命名空间中,增加名称和上一步创建的对象关联
单独运行下面的例子,体会区别

import functools  # 导入模块
print(1, dir())
print(2, functools)
print(3, functools.wraps)
~~~~~~~~~~~~~~~~~~~~~~~~~~
1 [..., 'functools']
2 <module 'functools' from 'C:\\Users\\SevenXue\\AppData\\Local\\Programs\\Python\\Python35\\lib\\functools.py'>
3 <function wraps at 0x0000017EA142EA60>
import os.path  # 导入os.path, os加入当前名词空间
print(1, dir())
print(2, os)
print(3, os.path)  # 完全限定名称访问path
~~~~~~~~~~~~~~~~~~~~~~
1 [..., 'os']
2 <module 'os' from 'C:\\Users\\SevenXue\\AppData\\Local\\Programs\\Python\\Python35\\lib\\os.py'>
3 <module 'ntpath' from 'C:\\Users\\SevenXue\\AppData\\Local\\Programs\\Python\\Python35\\lib\\ntpath.py'>
import os.path as osp  # 导入os.path并赋给osp
print(1, dir())
print(2, osp)
~~~~~~~~~~~~~~~~~~~~~~~~~~~
1 [..., 'osp']
2 <module 'ntpath' from 'C:\\Users\\SevenXue\\AppData\\Local\\Programs\\Python\\Python35\\lib\\ntpath.py'>
  • 总结
  • 导入顶级模块,其名称会加入到本地名词空间中,并绑定到其模块对象。
  • 导入非顶级模块,只将其顶级模块名称加入到本地名词空间中。导入的模块必须使用完全限定名称来访问。
  • 如果使用了as,as后的名称直接绑定到导入的模块对象,并将该名称加入到本地名词空间中。

from语句

语句

含义

from … import …

部分导入

from … import … as …

别名

from pathlib import Path, PosixPath  # 在当前名词空间导入该模块指定的成员
print(dir())
~~~~~~~~~~~~~~~~~~~~~~~~~~
['Path', 'PosixPath', ...]
from pathlib import *  # 当前名词空间导入该模块所有公共成员(非下划线开头成员)或指定成员
print(dir())
~~~~~~~~~~~~~~~~~~~~~~~~~
['Path', 'PosixPath', 'PurePath', 'PurePosixPath', 'PureWindowsPath', 'WindowsPath', ...]
from functools import wraps as wr, partial  # 别名
print(dir())
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
[..., 'partial', 'wr']
from os.path import exists  # 加载、初始化os、os.path模块,exists加入本地名词空间并绑定


if exists('d:/t'):
    print('Found')
else:
    print('Not Found')


print(1, dir())
print(2, exists)


import os


print(10, os.path.exists)
print(11, exists)
print(12, os.path.__dict__['exists'])
print(13, getattr(os.path, 'exists'))
# 上面4种方式获得同一个对象
~~~~~~~~~~~~~~~~~~~~~~~~~
Not Found
1 [..., 'exists']
2 <function exists at 0x0000024006D2C1E0>
10 <function exists at 0x0000024006D2C1E0>
11 <function exists at 0x0000024006D2C1E0>
12 <function exists at 0x0000024006D2C1E0>
13 <function exists at 0x0000024006D2C1E0>
  • 总结
  • 找到from子句中指定的模块,加载并初始化它(注意不是导入)
  • 对于import子句后的名称
  • 先查from子句导入的模块是否具有该名称的属性
  • 如果不是,则尝试导入该名称的子模块
  • 还没有找到,则抛出ImportError异常
  • 这个名称保存在本地名词空间中中,如果有as子句,则使用as子句后的名称
from pathlib import Path  # 导入类Path
print(1, Path, id(Path))


import pathlib as pl  # 导入模块使用别名
print(2, dir())
print(3, pl)
print(4, pl.Path, id(pl.Path))
# 可以看出导入的名词Path和pl.Path是同一个对象
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1 <class 'pathlib.Path'> 3082707106104
2 ['Path', '__builtins__', '__cached__', '__doc__', '__file__', '__loader__', '__name__', '__package__', '__spec__', 'pl']
3 <module 'pathlib' from 'C:\\Users\\SevenXue\\AppData\\Local\\Programs\\Python\\Python35\\lib\\pathlib.py'>
4 <class 'pathlib.Path'> 3082707106104

自定义模块

自定义模块:.py文件就是一个模块

# test1.py文件
print('This is test1 module')


class A:
    def showmodule(self):
        print("{}.a = {}".format(self.__module__, self))
        print(self.__class__.__name__)


a = A()
a.showmodule()


# test2.py文件
import test1

a = test1.A()
a.showmodule()


# test3.py文件
from test1 import A as cls


a = cls()
a.showmodule()

自动以模块命名规范

  • 模块名就是文件名
  • 模块名必须符合标识符的要求,是非数字开头的字母数字和下划线的组合,test-module.py这样的文件名不能作为模块名
  • 不要使用系统模块名来避免冲突,除非你明确知道这个模块名的用途
  • 通常名为全小写,下划线来分割

模块搜索顺序

使用sys.path查看搜索顺序

import sys

for p in sys.path:
    print(p)

显示结果为,python模块的路径搜索顺序
当加载一个模块的时候,需要从这些搜索路径中从前到后依次查找,并不搜索这些目录的子目录,搜索到模块就加载,搜索不到就抛异常
路径也可以为字典、zip文件、egg文件。
-egg文件,由setuptools库创建的包,第三方库常用的格式。添加了元数据(版本号、依赖项等)信息的zip文件
- 路径顺序为
- 程序主目录。程序运行的主程序脚本所在的目录
- PYTHONPATH目录,环境变量PYTHONPATH设置的目录也是搜索模块的路径
- 标准库目录,python自带的库模块所在目录

sys.path可以被修改,追加新的目录

模块的重复导入

# test1.py文件
print('This is test1 module')


class A:
    def showmodule(self):
        print("{}.a = {}".format(self.__module__, self))
        print(self.__class__.__name__)


a = A()
a.showmodule()


# test2.py文件
import test1

print('local module')

import test1
import test1

从执行结果来看,不会产生重复导入的现象。
所有加载的模块都会记录在sys.modules中,sys.modules是存储已经加载过的所有模块的字典。

# test2.py文件
import test1


print('local module')


import test1
import test1

import sys
print(sys.modules['test1'])
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
This is test1 module
test1.a = <test1.A object at 0x0000028D6D90A8D0>
A
local module
<module 'test1' from 'C:\\python10\\code\\Python10\\test1.py'>

模块运行

name,每个模块都会定义一个name特殊变量来存储当前模块的名称,如果不指定,则默认为源代码文件名,如果是包则有限定名。
解释器初始化的时候,会初始化sys.modules字典(保存已加载的模块),创建builtins(全局函数、常量)模块、main模块、sys模块,以及模块搜索路径sys.path
Python是脚本语言,任何一个脚本都可以直接执行,也可以作为模块被导入。
当从标准输入(命令行方式敲代码)、脚本($ python test.py)或交互式读取的时候,会将模块的name设置为的main,模块的顶层代码就在main这个作用域中执行。顶层代码,模块中缩进最外层的代码。
如果是import导入的,其name默认就是模块名

# test1.py文件
import test2


# test2.py文件
# 判断模块是否以程序的方式运行 $ python test.py
if __name__ == '__main__':
    print('in __main__')  # 程序的方式运行的代码
else:
    print('in import module')  # 模块导入的方式运行的代码

if __name__ == '__main__':用途
1. 本模块的功能测试
对于非主模块,测试本模块内的函数、类
2. 避免主模块变更的副作用
顶层代码,没有封装,主模块使用时没有问题,但是,一旦有了新的主模块,老的主模块成了被导入模块,由于原来代码没有封装,一并执行了。

模块的属性

属性

含义

__file__

字符串,源文件路径

__cached__

字符串,编译后的字节码文件路径

__spec__

显示模块的规范

__name__

模块名

__package__

当模块是包,同__name__;否则,可以设置为顶级模块的空字符串

包,特殊的模块
Python模块支持目录吗?
实验
项目中新建一个目录m,使用下面的代码

import m
print(m)
print(type(m))
print(dir(m))
~~~~~~~~~~~~~~~~~~~~~~~~~~
<module 'm' (namespace)>
<class 'module'>
['__doc__', '__loader__', '__name__', '__package__', '__path__', '__spec__']

竟然可以导入目录m,目录也是文件,所以可以导入,不过问题是,目录模块怎么写入代码?为了解决这个问题,Python要求在目录下简历一个特殊文件init.py,在其中写入代码
pycharm中,创建Directory和创建Python package不同,前者是创建普通的目录,后者是创建一个带有init.py文件的目录即包
Python中,目录可以作为模块,这就是包,不过代码需要写在该目录下init.py中

子模块

包目录下的py文件、子目录都是其子模块

m
|-- __init__.py
|-- m1.py
|-- m2
    |-- __init__.py
    |-- m21
        |-- __init__.py

如上建立子模块目录和文件,所有的py文件中就写一句话print(__name__)

# 注意查看模块的加载,当前名词空间
# import m
# import m.m1
# from m import m1
# from m.m2 import m21
import m.m2.m21

print(dir())

import sys
print(sorted(filter(lambda x:x.startswith('m'), sys.modules.keys())))
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
m
m.m2
m.m2.m21
['__builtins__', '__cached__', '__doc__', '__file__', '__loader__', '__name__', '__package__', '__spec__', 'm']
['m', 'm.m2', 'm.m2.m21', 'marshal']

删除init.py试一试,可以发现删除并不影响导入,但是这不是良好的习惯,请保留__init__py文件

模块和包的总结

包能够更好的组织模块,尤其是大的模块代码行数很多,,可以把它拆分成很多子模块,便于使用某些功能就加载相应的子模块。
包目录中init.py是在包第一次导入的时候就会执行,内容可以为空,也可以是用于该包初始化工作的代码,最好不要删除它(低版本不可删除)
导入子模块一定会加载父模块,但是导入父模块一定不会导入子模块
包目录之间只能使.点号作为间隔符,表示模块及其子模块的层级关系
模块也封装,如同类、函数,不过它能够封装变量、类、函数。
模块就是命名空间,其内部的顶层标识符,都是它的属性,可以通过dict或dir(module)查看。
包也是模块,但模块不一定是包,包是特殊的模块,是一种组织方式,它包含path属性

问题

from json import encoder之后,json.dump函数是否能使用,为什么?
import json.encoder之后呢?json.dump函数能用吗?

原因是from json import encoder之后,当前名词空间没有json,但是json模块已经加载过了,没有jison的引用,无法使用dump函数。
import json.encoder也加载json模块,但是当前名词空间有json,因此可以调用。

from json import encoder
print(dir())
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
[..., 'encoder']
import json.encoder
print(dir())
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
[..., 'json']

绝对导入,相对导入

绝对导入

在import语句或者from导入模块,模块名称最前面不是以.点开头的
绝对导入总是去搜索模块搜索路径中找

相对导入

只能在包内使用,且只能用在from语句中
使用.点号,表示当前目录中
..表示上一级目录
不要在顶层模块中使用相对导入

  • 举例a.b.c模块,c是模块,c的代码中,使用
  • from .import d # imports a.b.d
  • from ..import e # imports a.e
  • from .d import x # a.b.d.x
  • from ..e import x # a.e.x

… 三点表示上上一级

使用下面结构的包,体会相对导入的使用

m
|-- __init__.py
|-- m1.py
|-- m2
    |-- __init__.py
    |-- m21
        |-- __init__.py

测试一下有相对导入语句的模块,能够直接运行吗?
不能了,很好理解,使用相对导入的模块就是为了内部互相的引用资源的,不是为了直接运行的,对于包来说,正确的使用方式还是在顶级模块使用这些包。

访问控制

下划线开头的模块名

或者_开头的模块是否能够被导入呢?
创建文件名为_xyz.py或者__xyz.py测试
都可以成功的导入,因为他们都是合法的标识符,就可以用作模块名

模块内的标识符

# xyz.py
print(__name__)
A = 5
_B = 6
__C = 7

__my__ = 8


# test.py
import xyz

import sys
print(1, sorted(sys.modules.keys()))
print(2, dir())

print(3, xyz.A, xyz._B, xyz.__C, xyz.__my__)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
xyz
1 [..., 'xyz', 'zipimport']
2 [..., 'xyz']
3 5 6 7 8

普通变量、保护变量、私有变量、特殊变量,都没有被隐藏,也就是说模块内没有私有的变量,在模块中定义不做特殊处理。

from语句

# test.py
from xyz import A, _B as B, __my__, __C as C

import sys
print(1, sorted(sys.modules.keys()))
print(2, dir())

print(3, A, B, __my__, C)
~~~~~~~~~~~~~~~~~~~~~~~~
xyz
1 [..., 'xyz', 'zipimport']
2 ['A', 'B', 'C', ...]
3 5 6 8 7

依然可以使用from语句,访问所有变量

from … import * 和 __all__

使用from … import * 导入

# xyz.py
print(__name__)
A = 5
_B = 6
__C = 7

__my__ = 8


# test.py
from xyz import *

import sys
print(1, sorted(sys.modules.keys()))
print(2, dir())
print(3, locals()['A'])

A = 55
print(4, locals()['A'])
~~~~~~~~~~~~~~~~~~~~~~~~~~~
xyz
1 [..., 'xyz', 'zipimport']
2 ['A', ...]
3 5
4 55

结果只是导入了A,下划线开头的都没有导入

使用__all__

__all__是一个列表,元素是字符串,每一个元素都是一个模块内的变量名

# xyz.py
__all__ = ["X", "Y"]

print(__name__)
A = 5
_B = 6
__C = 7

__my__ = 8

X = 10
Y = 20


# test.py
from xyz import *

import sys
print(1, sorted(sys.modules.keys()))
print(2, dir())
# print(3, locals()['A'])
print(4, locals()['X'])
print(5, locals()['Y'])
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
xyz
1 [..., 'xyz', 'zipimport']
2 ['X', 'Y', ...]
4 10
5 20

修改__all__列表,加入下划线开头变量,看看什么效果

# xyz.py
__all__ = ["X", "Y", "_B", "__C"]

print(__name__)
A = 5
_B = 6
__C = 7

__my__ = 8

X = 10
Y = 20

# test.py
from xyz import *

import sys
print(1, sorted(sys.modules.keys()))
print(2, dir())
# print(3, locals()['A'])
print(4, locals()['X'])
print(5, locals()['Y'])
print(6, locals()['_B'])
print(7, locals()['__C'])
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
xyz
1 [..., 'xyz', 'zipimport']
2 ['X', 'Y', '_B', '__C', ...]
4 10
5 20
6 6
7 7

可以看到使用from xyz import * 导入__all__列表中的名称

包和子模块

m
|-- __init__.py
|-- m1.py
# __init__.py
print(__name__)
x = 1


# m1.py
print(__name__)
y = 5


# test.py中
# 如何访问到m1.py中的变量m1
# 访问到m.m1的变量y的几种实现
# 方法1, 直接导入m1模块
import m.m1
print(m.m1.y)

# 方法2,直接导入m.m1的属性y
from m.m1 improt y
print(y)

# 方法3,from m import *
# 该方法导入后,无法看到子模块m1,无法访问y
# 在__init__.py增加__all__ = ['x', 'm1'], 使用__all__提供导出的名称
from m import *
print(m1.y)

# 方法4,不使用__all__
# 在__init__.py增加from . import m1
from m import *
print(m1.y)

__init__.py中有什么变量,则使用from m import *加载什么变量,这依然符合模块的访问控制

# __init__.py
print(__name__)
x = 1

from .m1 import y as _z
print(dir())

总结

使用from xyz import *导入

  1. 如果模块没有__all__from xyz import *只导入非下划线开头的模块的变量。如果是包,子模块也不会导入,除非在__all__中设置,或__init__.py中使用相对导入
  2. 如果模块有__all__from xyz import *只导入__all列表中指定的名称,哪怕这个名词是下划线开头的,或者是子模块
  3. from xyz import *方式导入,使用简单,但是其副作用是导入大量不需要使用的变量,甚至有可能造成名称的冲突。而__all__可以控制被导入模块在这种导入方式下能够提供的变量名称,就是为了阻止from xyz import *导入过多的模块变量,从而避免冲突。因此,编写模块时,应该尽量加入__all__

from module import name1, name2 导入

这种方式的导入是明确的,哪怕是导入子模块,或者导入下划线开头的名称
程序员可以由控制的导入名称和其对应的对象

模块变量的修改

# xyz.py
print(__name__)
X = 10


# test2.py
import xyz

print(xyz.X)


# test.py
import xyz

print(xyz.X)
xyz.X = 50

import test2
~~~~~~~~~~~~~~~~~~~~~~~
xyz
10
50

模块对象是同一个,因此模块的变量也是同一个,对模块变量的修改,会影响所有使用者。
除非万不得已,或者明确知道自己在做什么,否则不要修改模块的变量。
前面学习过的猴子不定,也可以通过打补丁的方式,修改模块的变量、类、函数等内容。