在构建大型Python项目时经常会使用到__init__.py和__main__.py文件,通过它们可以灵活的管理模块和包。

1 __init__.py文件的应用

在Python项目中,如果某个文件夹被设置成包的形式,那么该文件夹中必须要包含一个__init__.py文件,即使它是空文件。当导入这个包时,__init__.py文件中的代码会被优先执行。

  • 构建层级包

封装一个包,确保每个目录下都定义了一个__init__.py文件, 例如:

ml_api/
    __init__.py
    regression/
        __init__.py
        linear_regression.py
        ridge_regression.py
        ...
    classification/
        __init__.py
        svm.py
        xgboost.py
        ...
run.py

这样就能够在其他项目模块执行各种import语句,如下所示:

import ml_api.classification.svm
from ml_api.classification import xgboost
import ml_api.regression import ridge_regression as ridge

__init__.py文件的目的是使得不同运行级别的包能够可选的初始化代码。如果执行了语句import ml_api那么文件ml_api/__init__.py将被导入,建立以ml_api为命名空间的内容。像import ml_api.classification.svm这样导入,那么文件ml_api/__init__.py和文件ml_api/classification/__init__.py将在文件ml_api/classification/svm.py导入之前导入。

  • 控制子模块导入

绝大部分时候让__init__.py文件为空就好,但是有些情况下可以包含代码,从而行使特殊的功能,如控制子模块导入。

# ml_api/classification/__init__.py
from . import svm
from . import xgboost

现在仅通过import ml_api.classification一行代码就可以替代import ml_api.classification.svmimport ml_api.classification.xgboost 两行代码的功能。

  • 对子模块进行重构

__init__.py还可以将多个模块合并到一个逻辑命名空间。程序模块可以通过变成包的形式分割成多个独立的文件,不妨考虑

# test_module.py
class C1:
    def f1(self):
        print("C1.f1")

class C2(C1):
    def f2(self):
        print("C2.f2")

如果想把test_module.py拆分成两个文件,然后分别定义成类,从而实现逻辑上独立。要做到这一点,首先需要使用test_module目录来替换文件test_module.py。 这这个目录下,创建如下文件:

test_module/
    __init__.py
    c1.py
    c2.py

在c1.py文件中插入以下代码:

# c1.py
class C1:
    def f1(self):
        print("C1.f1")

在c2.py文件中插入以下代码:

# c2.py
from .c1 import C1
class C2(C1):
    def f2(self):
        print("C2.f2")

最后,在 __init__.py 中,将上述两个文件聚合在一起:

# __init__.py
from .c1 import C1
from .c2 import C2

至此,test_module就整合为统一的逻辑模块

>>>import test_module
>>>c1 = test_module.C1()
>>>c1.f1()
C1.f1
>>>c2 = test_module.C2()
>>>c2.f2()
C2.f2

上面讨论的其实是包的设计问题,在一个大型的代码库中,每个功能模块都是独立的文件,用户如果想使用相应的功能按需导入即可

from package.module_1 import C1
from package.module_2 import C2
...

但是这样往往会导致用户的负担增加,因为他需要知道不同的功能代码位于包的哪个模块,通常情况下应该是将这些逻辑统一起来,使用一条import语句简化导入流程

from package import C1, C2

因此需要使用__init__.py文件来将每个独立的模块聚合在一起。再举一个亲身使用的例子,假设使用fastAPI作为后端搭建机器学习算法API,为了叙述方便,简化的目录结构如下

ml_api/
    ml/
        __init__.py
            regression.py
            classification.py
    run.py

构建回归算法的请求API

# regression.py
from fastapi import APIRouter
...
regression_app = APIRouter()
...

构建分类算法的请求API

# classification.py
from fastapi import APIRouter
...
classification_app = APIRouter()
...

使用__init__.py文件整合所有算法模块

# __init__.py
from .regression import regression_app
from .classification import classification_app

最后在run.py文件中统一所有的路由,组建完整的API

import uvicorn
from fastapi import FastAPI
...
from ml import regression_app, classification_app

app = FastAPI(...)
...
app.include_router(regression_app, prefix='/regression')
app.include_router(classification_app, prefix='/classification')

if __name__ == '__main__':
    uvicorn.run('run:app', host='0.0.0.0', port=55555, reload=True, debug=True)
  • 延迟加载

对于构建一个很大的包,如果在__init__.py文件中一次性导入了所有必需的模块,可能会引发一些问题。所以在某些特殊情况下,需要设计成当组件使用时才会被加载。以上一小节第一个项目为例,如果要实现这种逻辑,__init__.py文件需要改动一些代码:

# __init__.py
def C1:
    from .c1 import C1
    return C1()

def C2():
    from .c2 import C2
    return C2()

在这个版本中,只有当C1和C2函数被调用时才会加载C1和C2类,但是对于用户而言,使用方法上并不会有什么不同

>>>import test_module
>>>c1 = test_module.C1()
>>>c1.f1()
C1.f1

2 __main__.py文件的应用

在Python项目中,__main__.py相较于__init__.py文件的用法来说比较单一,一句话总结就是,如果想要使得某个文件夹能够执行,那么该文件夹中必须要包含一个__main__.py文件,否则就会抛出异常。假设项目目录结构如下

your_package/
    __init__.py
    __main__.py
    a.py
    b.py
    ...

然后在__main__.py文件中插入行使相关功能的代码,运行your_package即可

>>>python your_package