一、模块的四种形式
1、1 什么是模块?
模块是一系列功能的集合体,而函数是某一个功能的集合体,因此模块可以看成是一堆函数的集合体。一个py文件内部就可以放一堆函数,因此一个py文件就可以看成一个模块。如果这个py文件的文件名为module.py
,模块名则是module
。
1、2 模块的四种形式
在Python中,总共有以下四种形式的模块:
-
自定义模块:如果你自己写一个py文件,在文件内写入一堆函数,则它被称为自定义模块,即使用python编写的.py文件
-
第三方模块:已被编译为共享库或DLL的C或C++扩展
-
内置模块:使用C编写并链接到python解释器的内置模块
-
包:把一系列模块组织到一起的文件夹(注:文件夹下有一个init.py文件,该文件夹称之为包)
1、3 为什么要用模块?
-
用第三方或者内置的模块是一种拿来主义,可以极大地提升开发效率。
-
自定义模块,将我们自己程序中用到的公共功能,写入一个python文件,然后程序的各部分组件可以通过导入的方式来引用自定义模块的功能。
1、4 如何用模块
一般我们使用import和from...import...导入模块。
二、 import和from...import
一般使用import和from...import...导入模块。
以下述spam.py内的文件代码为例。
# spam.py
print('from the spam.py')
money = 1000
def read1():
print('spam模块:', money)
def read2():
print('spam模块')
read1()
def change():
global money
money = 0
2、1 import 模块名
import首次导入模块发生了3件事:
-
以模块为准创造一个模块的名称空间
-
执行模块对应的文件,将执行过程中产生的名字都丢到模块的名称空间
-
在当前执行文件中拿到一个模块名
模块的重复导入会直接饮用之前创造好的结果,不会重复执行模块的文件,即重复导入会发生:spam=spam=模块名称空间的内存地址
# run.py
import spam as sm
money = 111111
sm.money
sm.read1() # 'spam模块:1000'
sm.read2
sm.change()
print(money) # 1000
导入多个模块
import spam, time, os
# 推荐使用下述方式
import spam
import time
import os
2、2 from 模块名 import 具体的功能
from...import...首次导入模块发生了3件事:
-
以模块为准创造一个模块的名称空间
-
执行模块对应的文件,将执行过程中产生的名字都丢到模块的名称空间
-
在当前执行文件的名称空间中拿到一个名字,该名字直接指向模块中的某一个名字,意味着可以不用加任何前缀而直接使用
-
优点:不用加前缀,代码更加精简
-
缺点:容易与当前执行文件中名称空间中的名字冲突
导入文件内所有的功能:
# spam.py
__all__ = ['money', 'read1'] # 只允许导入'money'和'read1'
# run.py
from spam import * # 导入spam.py内的所有功能,但会受限制于__all__
2、3 import和from...import...的异同
-
两者都会执行模块对应的文件,两者都会产生模块的名称空间
-
两者调用功能时,需要跑到定义时寻找作用域关系,与调用位置无关
不同点
-
import需要加前缀;from...import...不需要加前缀
三、循环导入问题
3、1 什么是循环导入?
# m1.py
print('from m1.py')
from m2 import x
y = 'm1'
-
创建m2的名称空间
-
执行m2.py,将执行产生的名字丢到m2.py
-
在当前执行文件中拿到m2.x
# m2.py
print('from m2.py')
from m1 import y
x = 'm2'
-
创建m1的名称空间
-
执行m1.py,将执行产生的名字丢到m1.py
-
在当前执行文件中拿到m1.y
# run.py
import m1
-
创建m1的名称空间
-
执行m1.py,将执行产生的名字丢到m1.py
-
在当前执行文件中拿到m1
-
如果运行run.py,则会报错
ImportError: cannot import name 'y'
-
如果运行m1.py,则会报错
ImportError: cannot import name 'x'
-
如果运行m2.py,则会报错
ImportError: cannot import name 'y'
3、2 解决方案
我们可以使用函数定义阶段只识别语法的特性解决循环导入的问题,我们也可以从本质上解决循环导入的问题,但是最好的解决方法是不要出现循环导入。
3.2.1 方案一
# m1.py
print('from m1.py')
def func1():
from m2 import x
print(x)
y = 'm1'
# m2.py
print('from m2.py')
def func1():
from m1 import y
print(y)
x = 'm2'
3.2.2 方案二
# m1.py
print('from m1.py')
y = 'm1'
from m2 import x
# m2.py
print('from m2.py')
x = 'm2'
from m1 import y
四、模块的搜索路径
4、1 模块搜索路径的顺序
模块其实就是一个文件,如果要执行文件,首先就需要找到模块的路径(某个文件夹)。如果模块的文件路径和执行文件不在同一个文件目录下,我们就需要指定模块的路径。
模块的搜索路径指的就是在导入模块时需要检索的文件夹。
导入模块时查找模块的顺序是:
-
先从内存中已经导入的模块中寻找
-
内置的模块
-
环境变量sys.path中找
import sys
print(f"sys.path: {sys.path}")
'''
['/Users/mac/Desktop/video/python/day16',
'/Users/mac/Desktop/video/python',
'/Applications/anaconda3/lib/python36.zip',
'/Applications/anaconda3/lib/python3.6',
'/Applications/anaconda3/lib/python3.6/lib-dynload',
'/Applications/anaconda3/lib/python3.6/site-packages',
'/Applications/PyCharm.app/Contents/helpers/pycharm_matplotlib_backend']
'''
强调:sys.path的第一个值是当前执行文件的所在的文件夹
1.1 验证先从内存中找
# mmm.py
def f1():
print('from mmm.py f1')
# run.py
import time
import mmm
time.sleep(10)
import mmm
mmm.f1() # from mmm.py f1
1.2 验证先从内置中找
# run.py
import time
print(time) # <module 'time' (built-in)>
1.3 验证从sys.path中找
# run.py
import sys
sys.path.append(r'/Users/mac/Desktop/video/python')
print(sys.path)
import mmm
mmm.f1()
4、2 搜索路径以执行文件为准
假设我们有上述的目录结构的文件,文件内代码分别是:
# m1.py
import sys
print('模块m1中查看的结果',sys.path)
# import m2
from dir1 import m2
m2.f2()
# m2.py
import sys
print(sys.path)
def f2():
print('from f2')
# run.py
import sys
print('执行文件查看的结果:',sys.path)
from dir1 import m1
其中run.py文件的执行路径是/Users/mac/Desktop/video/python/day16/模块搜索路径练习
,如果我们在m1.py中直接使用import m2
导入m2会报错,而使用from dir1 import m2
导入m2则会成功,因为搜索路径以执行文件为准,dir1和run.py是同目录下的,因此run.py的环境变量能找到dir1;而m2和run.py不是同目录下的,因此run.py的环境变量无法直接找到m2。
五、Python文件的两种用途(了解)
python文件总共有两种用途,一种是执行文件;另一种是被当做模块导入。
编写好的一个python文件可以有两种用途:
-
脚本,一个文件就是整个程序,用来被执行
-
模块,文件中存放着一堆功能,用来被导入使用
# aaa.py
x = 1
def f1():
print('from f1')
def f2():
print('from f2')
f1()
f2()
# run.py
import aaa
如果直接运行run.py会直接运行aaa.py中的f1()
和f2()
,但是如果我们在aaa.py中加上if __name__ == '__main__':
这句话,则可以防止运行run.py时执行f1()
和f2()
。因为当aaa.py被直接执行,即当做执行文件的时候__name__ == '__main__'
; 在aaa.py被当做模块直接运行的时候__name__ == 'aaa'
。由此可以让aaa.py在不同的场景下有着不同的用法。
# aaa.py
x = 1
def f1():
print('from f1')
def f2():
print('from f2')
if __name__ == '__main__':
f1()
f2()
六、编译Python的文件(了解)
6、1 编译Python文件
为了提高加载模块的速度,强调强调强调:提高的是加载速度而绝非运行速度。python解释器会在pycache目录中下缓存每个模块编译后的版本,格式为:module.version.pyc。通常会包含python的版本号。例如,在CPython3.3版本下,spam.py模块会被缓存成pycache/spam.cpython-33.pyc。这种命名规范保证了编译后的结果多版本共存。
Python检查源文件的修改时间与编译的版本进行对比,如果过期就需要重新编译。这是完全自动的过程。并且编译的模块是平台独立的,所以相同的库可以在不同的架构的系统之间共享,即pyc使一种跨平台的字节码,类似于JAVA火.NET,是由python虚拟机来执行的,但是pyc的内容跟python的版本相关,不同的版本编译后的pyc文件不同,2.5编译的pyc文件不能到3.5上执行,并且pyc文件是可以反编译的,因而它的出现仅仅是用来提升模块的加载速度的,不是用来加密的。
# python解释器在以下两种情况下不检测缓存
1. 如果是在命令行中被直接导入模块,则按照这种方式,每次导入都会重新编译,并且不会存储编译后的结果(python3.3以前的版本应该是这样)
python -m spam.py
2. 如果源文件不存在,那么缓存的结果也不会被使用,如果想在没有源文件的情况下来使用编译后的结果,则编译后的结果必须在源目录下
sh-3.2 # ls
__pycache__ spam.py
sh-3.2 # rm -rf spam.py
sh-3.2 # mv __pycache__/spam.cpython-36.pyc ./spam.pyc
sh-3.2 # python3 spam.pyc
spam
# 提示:
1. 模块名区分大小写,foo.py与FOO.py代表的是两个模块
2. 你可以使用-O或者-OO转换python命令来减少编译模块的大小
-O转换会帮你去掉assert语句
-OO转换会帮你去掉assert语句和__doc__文档字符串
由于一些程序可能依赖于assert语句或文档字符串,你应该在在确认需要
的情况下使用这些选项。
3. 在速度上从.pyc文件中读指令来执行不会比从.py文件中读指令执行更快,只有在模块被加载时,.pyc文件才是更快的
4. 只有使用import语句是才将文件自动编译为.pyc文件,在命令行或标准输入中指定运行脚本则不会生成这类文件,因而我们可以使用compieall模块为一个目录中的所有模块创建.pyc文件
模块可以作为一个脚本(使用python -m compileall)编译Python源
python -m compileall /module_directory 递归着编译
如果使用python -O -m compileall /module_directory -l则只一层
命令行里使用compile()函数时,自动使用python -O -m compileall
详见:https://docs.python.org/3/library/compileall.html#module-compileall
6、2 批量生成.pyc文件
import compileall
compileall.compile_dir('$dir')
其中,$dir 为Python源代码所在的目录。
七、 包
7、1 什么是包?
包是模块的一种形式,包的本质就是一个含有.py
的文件的文件夹。
7、2 为什么要有包?
模块的第一个版本只有10个功能,但是未来在扩展版本的时候,模块名和用法应该最好不要去修改,但是这只是对使用者友好,而由于版本扩展,文件越来越大,模块设计者对模块的管理、维护会越来越复杂,因此我们可以使用包来扩展模块的功能。
7、3 如何用包?
3.1 模块和包
导入模块发生的三件事:
-
创建一个包的名称空间
-
执行py文件,将执行过程中产生的名字存放于名称空间中。
-
在当前执行文件中拿到一个名字aaa,aaa是指向包的名称空间的
导入包发生的三件事:
-
创建一个包的名称空间
-
由于包是一个文件夹,无法执行包,因此执行包下的.py文件,将执行过程中产生的名字存放于包名称空间中(即包名称空间中存放的名字都是来自于.py)
-
在当前执行文件中拿到一个名字aaa,aaa是指向包的名称空间的
导入包就是在导入包下的.py,并且可以使用以下两种方式导入:
-
import ...
-
from ... import...
3.2 扩展模块功能
如下我们如果需要扩展aaa.py模块,需要建立一个aaa的目录文件,并且删除aaa.py文件,将aaa.py修改成m1.py和m2.py两个文件,让模块的功能使用方法不改变。
# aaa.py
def func1():
pass
def func2():
pass
def func3():
pass
def func4():
pass
def func5():
pass
def func6():
pass
# m1.py
def func1():
pass
def func2():
pass
def func3():
pass
# m2.py
def func4():
pass
def func5():
pass
def func6():
pass
# run.py
import aaa
aaa.func1()
aaa.func2()
aaa.func3()
aaa.func4()
aaa.func5()
aaa.func6()
3.3 修改init.py文件
# aaa/.py
func1 = 111
func2 = 222
func3 = 333
func4 = 444
func5 = 555
func6 = 666
由于在__init__.py
中定义了func1,因此我们可以在run.py文件中导入func1,但是这个func1并不是我们想要的func1,因此需要修改__init__.py
文件,又由于执行文件run.py的环境变量不为aaa,因此直接使用import导入m1会报错,因此使用from导入。
# aaa/.py
from aaa.m1 import func1
from aaa.m2 import func2
# run.py
import aaa
print(aaa.func1())
print(aaa.func2())
3.4 导入包内包
aaa.bbb指向aaa内部的文件夹bbb包,如果我们需要导入bbb这个包。
# bbb/.py
from aaa import bbb
# run.py
import aaa
print(aaa.bbb)
3.5 导入包内包的模块
如果bbb包内部有m3.py,我们需要从run.py导入m3模块。
# bbb/.py
from aaa.bbb import m3
# run.py
import aaa
aaa.bbb.m3
3.6 绝对导入和相对导入
绝对导入:
# aaa/.py
from aaa.m1 import func1
from aaa.m2 import func2
相对导入:
-
.代表当前被导入文件所在的文件夹
-
..代表当前被导入文件所在的文件夹的上一级
-
...代表当前被导入文件所在的文件夹的上一级的上一级
from .m1 import func1
from .m2 import func2
四、注意事项
-
包内所有的文件都是被导入使用的,而不是被直接运行的
-
包内部模块之间的导入可以使用绝对导入(以包的根目录为基准)与相对导入(以当前被导入的模块所在的目录为基准),推荐使用相对导入
-
当文件是执行文件时,无法在该文件内用相对导入的语法,只有在文件时被当作模块导入时,该文件内才能使用相对导入的语法
-
凡是在导入时带点的,点的左边都必须是一个包,
import aaa.bbb.m3.f3
错误
五、练习
假设我们有如上的目录结构,文件内代码如下:
# m1.py
def f1():
print('from f1')
# m2.py
def f2():
print('from f2')
# m3.py
def f3():
print('from f3')
1.如果我们需要在run.py中实现以下逻辑代码,因此得在__init__.py
文件中如此做:
# run.py
import aaa
aaa.f1()
aaa.f2()
aaa.f3()
print(aaa.bbb)
# aaa/__init__.py
from .m1 import f1
from .m2 import f2
from .bbb.m3 import f3
from . import bbb
2.如果我们需要在run.py中实现以下逻辑,__init__.py
什么都不需要做:
# run.py
from aaa.bbb.m3 import f3
f3()
八、 软件开发目录规范
为了提高程序的可读性与可维护性,我们应该为软件设计良好的目录结构,这与规范的编码风格同等重要,简而言之就是把软件代码分文件目录。假设你要写一个ATM软件,你可以按照下面的目录结构管理你的软件代码:
ATM/
|-- core/
| |-- src.py # 业务核心逻辑代码
|
|-- api/
| |-- api.py # 接口文件
|
|-- db/
| |-- db_handle.py # 操作数据文件
| |-- db.txt # 存储数据文件
|
|-- lib/
| |-- common.py # 共享功能
|
|-- conf/
| |-- settings.py # 配置相关
|
|-- bin/
| |-- run.py # 程序的启动文件,一般放在项目的根目录下,因为在运行时会默认将运行文件所在的文件夹作为sys.path的第一个路径,这样就省去了处理环境变量的步骤
|
|-- log/
| |-- log.log # 日志文件
|
|-- requirements.txt # 存放软件依赖的外部Python包列表,详见https://pip.readthedocs.io/en/1.1/requirements.html
|-- README # 项目说明文件
# run.py
import sys
import 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()
# 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.txt')
LOG_PATH = os.path.join(BASE_DIR, 'log', 'user.log')
# print(DB_PATH)
# print(LOG_PATH)
# src.py
from conf import settings
from lib import common
def login():
print('登陆')
def register():
print('注册')
name = input('username>>: ')
pwd = input('password>>: ')
with open(settings.DB_PATH, mode='a', encoding='utf-8') as f:
f.