如何在python项目中使用import导入自编模块
背景是在python项目中导入模块时碰到的问题,当需要导入的模块是位于项目的不同层级的时候,导入文件就变成了一个非常麻烦的事情。
下面举一个例子
. # 当前工作目录
├── DB
│ ├── MySQLClient.py
│ └── RedisClient.py
└── Uti
│ └── LogHandler.py
├── Email
│ └── SendEmail.py # 本文件
│ └── Email_setting.py # 待导入文件
2
3
4
5
6
7
8
9
DB 和 Util 是我们经常使用到的模块,若是我们的工作目录位于项目文件的第一层的话,当我们导入 Email_setting.py
的时候,我们应该是
from DB.MySQLClient import MySQLClass
from Util.LogHandler import LogHandler
from Email_setting import * # 这是错误的例子
from Email.Email_setting import *
2
3
4
下面我们讨论一下如何在python中导入模块
{% blockquote %}
参考自: Python 101: All about imports (opens new window)
{% 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
2
3
以上两个来源的库用起来比较省心,因为它们的目录已经加入了环境变量中了
# 查看环境变量的方式
>>> 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']
2
3
4
# 从模块导入
from functools import lru_cache
lru_cache(*args)
2
同样,你也可以导入该模块中的所有函数和变量,只是这种导入方式是不被推荐的
from os import *
官方建议我们,需要对 import
的函数,要显式的写出,但是当函数过多的时候,我们可能写成多行的形式
from os import path, walk, unlink
from os import uname, remove
2
为了能用一个 import 实现,我们可以利用 括号
帮助,或者 \
from os import (path, walk, unlink, uname,
remove, rename)
from os import path, walk, unlink, uname, \
remove, rename
2
3
4
# 相对导入
当使用的是绝对路径时,容易出现的问题是,在大型的项目中,当你改变包结构的时候,你需要对你的代码进行大幅度的修改。
另外,如果没有相对路径,那么包内的模块无法轻松导入自身。
# 一些例子
from .foo import bar
from ...foo import bar
2
这两种形式有两种不同的建议语义。一种语义是使每个点代表一个级别。但是数需要多少个点也是一件麻烦事。
另一种选择是只允许一个相对导入级别,这样的话又会制约模块的功能。
最后的选择是定义一种算法,用于查找相关的模块和软件包。 这里的反对意见是“明确胜于隐含”。 (建议的算法是“从当前程序包目录中搜索,直到最终的父程序包被命中为止。”)
# 只导入兄弟模块
一种建议是,只导入 兄弟
模块。换言之,对于更高层级的模块,使用绝对路径
from .spam import eggs
import .spam.eggs
2
# 使用索引父节点
from -2.spam import eggs # 高层级
from .spam import eggs # 本地
2
# 把代码组织成很多分层模块的包
使用一个 leading dot 作为相对路径,两个或以上代表父目录。
这里我们把整个项目作为一个包来看待,需要对每一层写一个
__init__.py
文件 这里,我们整个项目可以看成package
包,和subpackage1
和subpackage2
两个子包。
实现的方法很简单,就是确保在每一层目录上添加一个 __init__.py
文件
下面是我们的目录结构
package/
__init__.py
subpackage1/
__init__.py
moduleX.py # 当前文件
moduleY.py
subpackage2/
__init__.py # 当前文件
moduleZ.py
moduleA.py
2
3
4
5
6
7
8
9
10
假设 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
2
3
4
5
6
7
8
{% 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)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
我们切换到 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
2
3
4
5
6
7
# 可选导入
可选导入用的情况比较少,一般是用在需要导入一个模块,但是这个模块并不一定存在的情况。比如我们使用的python 版本不一致的时候,需要导入的模块也会有所不同,这样的写法能加强模块的健壮性=
# github2
下面是一段来自 github2 (opens new window) 的例子
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()])
2
3
4
5
6
7
8
9
# lxml
下面是一段来自 lxml package (opens new window) 的例子
try:
from urlparse import urljoin
from urllib2 import urlopen
except ImportError:
# Python 3
from urllib.parse import urljoin
from urllib.request import urlopen
2
3
4
5
6
7
# 本地导入
导入的模块分为 local scope
和 global 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))
2
3
4
5
6
7
8
9
10
11
12
13
14
# 导入的注意事项
容易犯错误的主要有两点
- 循环导入
- Shadowed imports
# 循环导入
简言之,就是模块相互导入
# a.py
import b
def a_test():
print("in a_test")
b.b_test()
a_test()
2
3
4
5
6
7
8
我们在相同的文件夹下,创建一个文件 b.py
import a
def b_test():
print('In test_b"')
a.a_test()
b_test()
2
3
4
5
6
7
如果是你运行这些模块的话,你将会获得 AttributeError 报错。虽然有一些旁门左道的变通方法可以解决,但是还是建议重构代码。
# 覆盖导入(Shadowed imports)
覆盖导入是指导入一个和官方库起名一样的模块,会报错。
主要的原因是,python 会首先搜索本地文件夹下的模块,其次是搜索其他路径。
import math
def square_root(number):
return math.sqrt(number)
square_root(72)
2
3
4
5
6