跳到主要内容

如何在python项目中使用import导入自编模块

背景是在python项目中导入模块时碰到的问题,当需要导入的模块是位于项目的不同层级的时候,导入文件就变成了一个非常麻烦的事情。

下面举一个例子

. # 当前工作目录
├── DB
│   ├── MySQLClient.py
│   └── RedisClient.py
└── Uti
│ └── LogHandler.py
├── Email
│ └── SendEmail.py # 本文件
│ └── Email_setting.py # 待导入文件

DB 和 Util 是我们经常使用到的模块,若是我们的工作目录位于项目文件的第一层的话,当我们导入 Email_setting.py 的时候,我们应该是

from DB.MySQLClient import MySQLClass
from Util.LogHandler import LogHandler
from Email_setting import * # 这是错误的例子
from Email.Email_setting import *

下面我们讨论一下如何在python中导入模块

{% blockquote %}

参考自: Python 101: All about imports

{% endblockquote %}

导入 Python 模块的各种姿势

  • Regular imports
  • Using from
  • Relative imports
  • Optional imports
  • Local imports
  • import Pitfalls

常规导入

我们最常见的导入方式是 import module ,我们一般用来导入 官方库第三方库

import math # 官方库
from bs4 import Beautfiul # 第三方库
import pandas as pd

以上两个来源的库用起来比较省心,因为它们的目录已经加入了环境变量中了

# 查看环境变量的方式
>>> import sys
>>> sys.path
['', '/Users/ppsteven/anaconda3/lib/python37.zip', '/Users/ppsteven/anaconda3/lib/python3.7', '/Users/ppsteven/anaconda3/lib/python3.7/lib-dynload', '/Users/ppsteven/anaconda3/lib/python3.7/site-packages', '/Users/ppsteven/anaconda3/lib/python3.7/site-packages/aeosa', '/Users/ppsteven/anaconda3/lib/python3.7/site-packages/xgboost-1.0.0_SNAPSHOT-py3.7.egg']

从模块导入

from functools import lru_cache
lru_cache(*args)

同样,你也可以导入该模块中的所有函数和变量,只是这种导入方式是不被推荐的

from os import *

官方建议我们,需要对 import 的函数,要显式的写出,但是当函数过多的时候,我们可能写成多行的形式

from os import path, walk, unlink
from os import uname, remove

为了能用一个 import 实现,我们可以利用 括号 帮助,或者 \

from os import (path, walk, unlink, uname, 
remove, rename)
from os import path, walk, unlink, uname, \
remove, rename

相对导入

当使用的是绝对路径时,容易出现的问题是,在大型的项目中,当你改变包结构的时候,你需要对你的代码进行大幅度的修改。

另外,如果没有相对路径,那么包内的模块无法轻松导入自身。

一些例子

from .foo import bar
from ...foo import bar

这两种形式有两种不同的建议语义。一种语义是使每个点代表一个级别。但是数需要多少个点也是一件麻烦事。

另一种选择是只允许一个相对导入级别,这样的话又会制约模块的功能。

最后的选择是定义一种算法,用于查找相关的模块和软件包。 这里的反对意见是“明确胜于隐含”。 (建议的算法是“从当前程序包目录中搜索,直到最终的父程序包被命中为止。”)

只导入兄弟模块

一种建议是,只导入 兄弟 模块。换言之,对于更高层级的模块,使用绝对路径

from .spam import eggs
import .spam.eggs

使用索引父节点

from -2.spam import eggs # 高层级
from .spam import eggs # 本地

把代码组织成很多分层模块的包

使用一个 leading dot 作为相对路径,两个或以上代表父目录。

这里我们把整个项目作为一个包来看待,需要对每一层写一个 __init__.py 文件 这里,我们整个项目可以看成 package 包,和 subpackage1subpackage2 两个子包。

实现的方法很简单,就是确保在每一层目录上添加一个 __init__.py 文件

下面是我们的目录结构

package/
__init__.py
subpackage1/
__init__.py
moduleX.py # 当前文件
moduleY.py
subpackage2/
__init__.py # 当前文件
moduleZ.py
moduleA.py

假设 moduleX.py__init__.py 是我们的当前文件,那么正确的导入的做法是

from .moduleY import spam
from .moduleY import spam as ham
from . import moduleY
from ..subpackage1 import moduleY
from ..subpackage2.moduleZ import eggs
from ..moduleA import foo
from ...package import bar
from ...sys import path

{% blockquote %}

相对路径必须使用 from <> import

绝对路径使用 import <>

{% endblockquote %}

# my_package/__init__.py
from . import subpackage1
from . import subpackage2

# my_package/subpackage1/__init__.py
from . import module_x
from . import module_y

# my_package/subpackage1/module_x.py
from .module_y import spam as ham

def main():
ham()
# my_package/subpackage1/module_y.py
def spam():
print('spam ' * 3)

我们切换到 my_package 上一层的目录,运行下面代码是正常的。

In [1]: import my_package

In [2]: my_package.subpackage1.module_x
Out[2]: <module 'my_package.subpackage1.module_x' from 'my_package/subpackage1/module_x.py'>

In [3]: my_package.subpackage1.module_x.main()
spam spam spam

可选导入

可选导入用的情况比较少,一般是用在需要导入一个模块,但是这个模块并不一定存在的情况。比如我们使用的python 版本不一致的时候,需要导入的模块也会有所不同,这样的写法能加强模块的健壮性=

github2

下面是一段来自 github2 的例子

try:
# For Python 3
from http.client import responses
except ImportError: # For Python 2.5-2.7
try:
from httplib import responses # NOQA
except ImportError: # For Python 2.4
from BaseHTTPServer import BaseHTTPRequestHandler as _BHRH
responses = dict([(k, v[0]) for k, v in _BHRH.responses.items()])

lxml

下面是一段来自 lxml package 的例子

try:
from urlparse import urljoin
from urllib2 import urlopen
except ImportError:
# Python 3
from urllib.parse import urljoin
from urllib.request import urlopen

本地导入

导入的模块分为 local scopeglobal scope 空间。当你在 python script 的头部导入的时候,作用在全局域。当在函数中导入的时候是本地域。

import sys  # global scope

def square_root(a):
# This import is into the square_root functions local scope
import math
return math.sqrt(a)

def my_pow(base_num, power):
# 这里直接使用 math 会报错的
return math.pow(base_num, power)

if __name__ == '__main__':
print(square_root(49))
print(my_pow(2, 3))

导入的注意事项

容易犯错误的主要有两点

  • 循环导入
  • Shadowed imports

循环导入

简言之,就是模块相互导入

# a.py
import b

def a_test():
print("in a_test")
b.b_test()

a_test()

我们在相同的文件夹下,创建一个文件 b.py

import a

def b_test():
print('In test_b"')
a.a_test()

b_test()

如果是你运行这些模块的话,你将会获得 AttributeError 报错。虽然有一些旁门左道的变通方法可以解决,但是还是建议重构代码。

覆盖导入(Shadowed imports)

覆盖导入是指导入一个和官方库起名一样的模块,会报错。

主要的原因是,python 会首先搜索本地文件夹下的模块,其次是搜索其他路径。

import math

def square_root(number):
return math.sqrt(number)

square_root(72)

参考资料

Python 101: All about imports