我们现在已经总结了Python的基本招式和套路,现在可以写一些不那么简单的系统性工程或代码量较大的应用程序。这时候,一个简单的.py文件就会显得过于臃肿,无法承担一个重量级软件开发的重任。这就需要这一章的内容——化繁为简,将功能模块化、文件化,从而可以像搭积木一样,将不同的功能,组建在大型工程中搭建起来。

简单模块化

  最简单的模块化方式,就是把函数、类、常量拆分到不同的文件,把他们放在同一个文件夹,然后使用下面的语句导入

from filename import function_name
from filename import 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(2, 3))

encoder = Encoder()
decoder = Decoder()

print(encoder.encode('abc'))
print(decoder.decode('dcba'))

我们把函数get_sum()放在一个文件里(utils.py),而类放在另外一个文件中(class_utils.py),在main函数里就直接调用,from...import...就可以把所需要的类和函数都导入进来。

可是慢慢的,我们发现把所有的文件都放在一个文件夹里也是不好管理的,需要建立多级的目录,就像这样

1 .
2 ├── utils
3 │   ├── utils.py
4 │   └── class_utils.py
5 ├── src
6 │   └── sub_main.py
7 └── main.py

main.py调用子目录的模块时,就需要改变一下代码了

#sub_main.py

import sys
sys.path.append('..')

from utils.class_utils import *
from utils import utils as fun

print(fun.get_sum(2, 3))

encoder = Encoder()
decoder = Decoder()

print(encoder.encode('abc'))
print(decoder.decode('dcba'))

sub_main函数调用的是上层目录下的子目录,就要把这个目录('..')加载到环境变量里。导入的方法也有所不同,导入的方法和模块使用方法可以看看前面写的文章:python之模块的导入。

要注意的一点,import同一个模块指挥被执行一次,这样就可以防止重复导入模块出现问题。在很多公司的编程规范中,除了一些极其特殊的情况,import必须位于程序的最前端

还有一点,在许多教程中,我们都要在模块所在的文件夹里建一个__init__.py的文件,内容也可以是空的,主要用来描述包(package)对外暴露的模块接口。不过这是Python2的规范。在Python3以后的版本中,__init__.py并不是必须的。

项目模块化

  很多大的项目,一个项目组的workspace可能有上千个文件,几十万到几百万行代码,上面所说的调用方式已经完全不够用了,下面我们就了解一下模块化的科学组织方式

  首先要回顾一下绝对路径和相对路径的概念。我们在下面的路径下有两个文件

a1/b1/c1/d1/e1/example.txt
a1/b1/c1/d2/e2/example2.txt

如果我们要从e1跳转到e2文件夹,有两种方法

#方法1
cd a1/b1/c1/d2/e2
#方法2
../../d2/e2

方法1用的就是绝对路径,而方法2就是相对路径。其中,用点点(../)代表上一级目录。

一个确定的路径对大型工程来说是非常必要的

import sys
sys.path.append('..')

  首先,因为代码可能会迁移,相对为hi会使得重构既不雅观,也容易出错。因此,在大型的工程中尽可能使用绝对位置是第一要义。对于一个独立的项目,所有的模块的追寻方式,最好从项目的根目录开始追溯,这叫做相对的绝对路径。下面我们创建一个新的项目,项目的结构如下:

1 .
2 ├── proto
3 │   ├── mat.py
4 ├── utils
5 │   └── mat_mul.py
6 └── src
7     └── 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]]

这个案例和前面的例子很像,但是导入的方式是直接用proto.mat导入的因为使用pycharm构建的项目,把不同文件放在不同的子文件夹里,跨模块调用是直接从顶层搜索的。所以必须是新建的项目,不能在已建的项目里新建个文件夹。

但是当我们用命令行调用src文件夹下,执行

python main.py

##########输出##########
Traceback (most recent call last):
  File "main.py", line 2, in <module>
    from proto.mat import Matrix
ModuleNotFoundError: No module named 'proto'

错了吧!因为Python解释器在遇到import的时候,他会在一个特定的列表里寻找模块,这个列表我们可以看一下

import sys

print(sys.path)

Pycharm在细腻教案项目是就是把根目录设置为列表的里的一项(列表太长了我就不展示了)。这样我们在运行main.py时,import都会从根目录里找相应的包。

那普通的Python运行环境怎么办呢?

第一种:大力出奇迹——强行修改sys.path列表。把根目录直接加进去

import sys
sys.path.append(r'根目录路径')

这样我们的import就畅通无阻了。但是把绝对路径写在代码里是一个非常不推荐的方式(写在配置文件中也会因为找配置文件也需要个路径,于是就进入了个死循环)

第二种方法:修改PYTHONHOME。这里可以提一下Python的Virtual Environment,Python可以通过Virtualenv工具非常方便的常见一个全新的Python运行环境。事实上,对于每一个Python项目来说,最好要有一个独立的运行环境来保持包和模块的纯净性。

在一个虚拟环境里,能找到一个文件叫active,在这个文件的末尾,可以加上下面的内容

export PYTHONPATH='项目路径‘

这样每次通过active激活这个运行环境的时候就会自动将项目的根目录添加到搜索路径中去。

神奇的if __name__ == '__main__'

最后我们来看一下一个非常常见的写法:

if __name__ == '__main__':

这个语法有什么用呢?Python是脚本预压,和C++、Ja最大的不同就是不需要显性的提供main()函数入口。

但是我们在导入文件的过程中,会把所有暴露在外面的代码统统执行一遍。但是大多数时候我们是不想让他们执行的,这时候如果我们想要把一个东西封装成模块,又想让他在需要的时候才执行的话,就把必须要执行的代码放在if __name__ == '__main__'里。因为在__name__是一个内置参数,在我们使用import导入的时候,__name__就会被赋值为该模块的名字,当然就不等于'__main__'了!

思考题

 我们在导入的时候有这两种方式

#方式A
from module_name import *
#方式B
import module_name

正常使用的时候哪种比较适合呢?

第一种会把路径下所有的模块导入程序,这就存在一个问题:如果有其他的函数名或类名和导入的模块相同,很容易造成冲突。

而第二种在使用的时候需要用下面的方式调用。相当于加了一层layer,能有效的避免因为名字相同导致的冲突。

model_name.fun()