当我们尝试写一些不那么简单的系统性工程,或者代码量较大的应用程序,这时简单的一个 py 文件已经过于臃肿,无法承担一个重量级软件开发的重任。这时候就需要我们化繁为简,将功能模块化、文件化,从而可以像搭积木一样,将不同的功能,组件在大型工程中搭建起来。
简单模块化
最简单的模块化方式,你可以把函数、类、常量拆分到不同的文件,把它们放在同一个文件夹
然后使用 from your_file import function_name, class_name 的方式调用。之后,这些函数和类就可以在文件内直接使用了。
# utils.py
def get_sum(a, b):
return a + b
# class_utils.py
class Encoder(object):
def encode(self, s):
return s[::-1]
class Decoder(object):
def decode(self, s):
return ''.join(reversed(list(s)))
# main.py
from utils import get_sum
from class_utils import *
print(get_sum(1, 2))
encoder = Encoder()
decoder = Decoder()
print(encoder.encode('abcde'))
print(decoder.decode('edcba'))
########## 输出 ##########
3
edcba
abcde
-
get_sum() 函数定义在 utils.py,
-
Encoder 和 Decoder 类则在 class_utils.py,
-
我们在 main 函数直接调用 from import ,就可以将我们需要的东西 import 过来。
但所有文件都堆在一个文件夹下也并不是办法,于是,我们试着建一些子文件夹:
# utils/utils.py
def get_sum(a, b):
return a + b
# utils/class_utils.py
class Encoder(object):
def encode(self, s):
return s[::-1]
class Decoder(object):
def decode(self, s):
return ''.join(reversed(list(s)))
# src/sub_main.py
import sys
sys.path.append("..")
from utils.class_utils import *
encoder = Encoder()
decoder = Decoder()
print(encoder.encode('abcde'))
print(decoder.decode('edcba'))
########## 输出 ##########
edcba
abcde
文件结构是下面这样的:
.
├── utils
│ ├── utils.py
│ └── class_utils.py
├── src
│ └── sub_main.py
└── main.py
main.py 调用子目录的模块时,只需要使用 . 代替 / 来表示子目录,utils.utils 表示 utils 子文件夹下的 utils.py 模块。
但是在sub_main.py中,我们如何调用上层目录呢?sys.path.append("..") 表示将当前程序所在位置向上提了一级,之后就能调用 utils 的模块了。
同时要注意,import 必须位于程序的最前端,import 同一个模块只会被执行一次,防止重复导入模块出现问题,良好的编程习惯应该杜绝代码多次导入的情况。
此外,Python 2 的规范:需要在模块所在的文件夹新建一个 init.py,内容可以为空,也可以用来表述包对外暴露的模块接口。但在 Python 3 规范中,init.py 并不是必须的
相对路径与绝对路径
在 Linux 系统中,每个文件都有一个绝对路径,以 / 开头,来表示从根目录到叶子节点的路径,
/home/ubuntu/Desktop/my_project/test.py
/home/ubuntu/Downloads/example.json
这种表示方法叫作绝对路径。
对于任意两个文件,都有一条通路可以从一个文件走到另一个文件, 例如我们从test.py访问到example.json
可以写成../../Downloads/example.json
其中..表示上一层目录,这种表示方法,叫作相对路径。
通常一个Python文件在运行的时候,都会有一个运行时的位置,最开始时即为这个文件所在的文件夹
这个运行路径可以被改变。运行sys.path.append("..")可以改变当前Python解释器的位置
不过最好不要用,固定一个确定路径对大型工程来说是非常必要的
理解这些概念后,我们就容易搞明白项目中如何设置模块的路径
首先,相对位置是一种很不好的选择。项目代码可能会迁移,相对位置会使得重构极不雅观,也容易出错。
因此在大型工程中尽可能使用绝对位置是第一要义
对于一个独立的项目,所有模块的追寻方式,最好从项目的根目录开始追溯,这叫做相对的绝对路径
整个公司都只有一个代码仓库,会有以下优点
- 简化依赖管理。整个公司的代码模块,都可以被你写的任何程序所调用,而你写的库和模块也会被其他人调用。调用的方式,都是从代码的根目录开始索引,也就是相对的绝对路径。这样极大地提高了代码的分享共用能力,你不需要重复造轮子,只需要在写之前,去搜一下有没有已经实现好的包或者框架就行
- 版本统一。不存在使用了一个新模块,却导致一系列函数崩溃的情况;并且所有的升级都需要通过单元测试才可以继续
- 代码追溯。可以很容易追溯一个API是从哪里被调用的,它的历史版本是怎样迭代开发,产生变化的
Python项目模块化
模块化的思想 ——以项目的根目录作为最基本的目录,所有的模块调用,都要通过根目录一层层向下索引的方式来import
我们使用Pycharm来创建一个项目
.
├── proto
│ ├── mat.py
├── utils
│ └── mat_mul.py
└── src
└── main.py
# proto/mat.py
class Matrix(object):
def __init__(self, data):
self.data = data
self.n = len(data)
self.m = len(data[0])
# utils/mat_mul.py
from proto.mat import Matrix
def mat_mul(matrix_1: Matrix, matrix_2: Matrix):
assert matrix_1.m == matrix_2.n
n, m, s = matrix_1.n, matrix_1.m, matrix_2.m
result = [[0 for _ in range(n)] for _ in range(s)]
for i in range(n):
for j in range(s):
for k in range(m):
result[i][k] += matrix_1.data[i][j] * matrix_2.data[j][k]
return Matrix(result)
# src/main.py
from proto.mat import Matrix
from utils.mat_mul import mat_mul
a = Matrix([[1, 2], [3, 4]])
b = Matrix([[5, 6], [7, 8]])
print(mat_mul(a, b).data)
########## 输出 ##########
[[19, 22], [43, 50]]
请注意utils/mat_mul.py,它 import Matrix 的方式是from proto.mat。
这种做法直接从项目根目录中导入,并依次向下导入模块 mat.py 中的 Matrix,而不是使用 .. 导入上一级文件夹。
把不同模块放在不同子文件夹里,跨模块调用则是从顶层直接索引,一步到位,非常方便
如果我们尝试使用命令行进入src文件夹,直接输入Python main.py,报错找不到proto
退回到上一级目录,输入Python src/main/py 继续报错,找不到proto
实际上,Python解释器在遇到import的时候,它会在一个特定的列表中寻找模块
import sys
print(sys.path)
########## 输出 ##########
['', '/usr/lib/python36.zip', '/usr/lib/python3.6', '/usr/lib/python3.6/lib-dynload', '/usr/local/lib/python3.6/dist-packages', '/usr/lib/python3/dist-packages']
第一项为空。Pycharm做的就是把第一项设置为项目根目录的绝对地址,这样每次无论怎么运行main.py,import函数在执行的时候,都回去项目根目录中找相应的包
如何使普通的Python运行环境也能做到?
第一种方法,我们可以强行修改这个位置,这样import接下来肯定就畅通无阻。但把绝对路径写在代码里,是非常不推荐的方式
import sys
sys.path[0] = '/home/ubuntu/workspace/your_projects'
第二种方法,是修改PYTHONHOME,在一个Virtual Environment里,能找到一个文件叫activate,在这个文件的末尾,填上下面的内容
export PYTHONPATH="/home/ubuntu/workspace/your_projects"
这样每次通过activate激活这个运行环境时,它就会自动将项目的根目录添加到搜索路径中
if name == 'main'
Python是脚本语言,和C++、Java最大的不同在于不需要显示提供main()函数入口
项目结构如下:
.
├── utils.py
├── utils_with_main.py
├── main.py
└── main_2.py
# utils.py
def get_sum(a, b):
return a + b
print('testing')
print('{} + {} = {}'.format(1, 2, get_sum(1, 2)))
# utils_with_main.py
def get_sum(a, b):
return a + b
if __name__ == '__main__':
print('testing')
print('{} + {} = {}'.format(1, 2, get_sum(1, 2)))
# main.py
from utils import get_sum
print('get_sum: ', get_sum(1, 2))
########## 输出 ##########
testing
1 + 2 = 3
get_sum: 3
# main_2.py
from utils_with_main import get_sum
print('get_sum: ', get_sum(1, 2))
########## 输出 ##########
get_sum_2: 3
import在导入文件的时候,会自动把所有暴露在外面的代码全部执行一遍。
因此,如果你要把一个东西封装成模块,又想让它可以执行的话,必须要将执行的代码放在if __name__ == '__main__'下面
__name__作为Python的魔术内置函数,本质上是模块对象的一个属性,使用import语句时,__name__就会被赋值为该模块的名字,自然就不等于__main__了
from module_name import * 和 import module_name的区别
- from module_name import * 会把 module 中所有的函数和类全拿过来,如果和其他函数名类名有冲突就会出问题
- import model_name 也会导入所有函数和类,但是调用的时候必须使用 model_name.func 的方法来调用,等于增加了一层 layer,有效避免冲突。
总结
- 通过绝对路径和相对路径,我们可以 import 模块;
- 在大型工程中模块化非常重要,模块的索引要通过绝对路径来做,而绝对路径从程序的根目录开始;
- 记着巧用if name == 'main'来避开 import 时执行。