轉(zhuǎn): https:///posts/python-de-import-ji-zhi.html 一直對 Python 的 import 機(jī)制不甚了解,這次在寫 Easy-Karabiner 的時候就踩坑了,順便了解了一下,發(fā)現(xiàn)這玩意兒還不是那么「符合直覺」,遂寫篇博客講講。
模塊與包
在了解 import 之前,有兩個概念必須提一下:
- 模塊: 一個
.py 文件就是一個模塊(module)
- 包:
__init__.py 文件所在目錄就是包(package)
當(dāng)然,這只是極簡版的概念。實際上包是一種特殊的模塊,而任何定義了 __path__ 屬性的模塊都被當(dāng)做包。只不過,咱們?nèi)粘J褂弥胁⒉恍枰肋@些。
兩種形式的 import
import 有兩種形式:
import ...
from ... import ...
兩者有著很細(xì)微的區(qū)別,先看幾行代碼。
| from string import ascii_lowercase
import string
import string.ascii_lowercase
|
運行后發(fā)現(xiàn)最后一行代碼報錯:ImportError: No module named ascii_lowercase ,意思是:“找不到叫 ascii_lowercase 的模塊”。第 1 行和第 3 行的區(qū)別只在于有沒有 from ,翻翻語法定義發(fā)現(xiàn)有這樣的規(guī)則:
import ... 后面只能是模塊或包
from ... import ... 中,from 后面只能是模塊或包,import 后面可以是任何變量
可以簡單的記成:第一個空只能填模塊或包,第二個空填啥都行。
import 的搜索路徑
提問,下面這幾行代碼的輸出結(jié)果是多少?
| # foo.py
import string
print(string.ascii_lowercase)
|
是小寫字母嗎?那可不一定,如果目錄樹是這樣的:
| ./
├── foo.py
└── string.py
|
foo.py 所在目錄有叫 string.py 的文件,結(jié)果就不確定了。因為你不知道 import string 到底是 import 了 ./string.py 還是標(biāo)準(zhǔn)庫的 string 。為了回答這個問題,我們得了解一下 import 是怎么找到模塊的,這個過程比較簡單,只有兩個步驟:
- 搜索「內(nèi)置模塊」(built-in module)
- 搜索
sys.path 中的路徑
而 sys.path 在初始化時,又會按照順序添加以下路徑:
foo.py 所在目錄(如果是軟鏈接,那么是真正的 foo.py 所在目錄)或當(dāng)前目錄;
- 環(huán)境變量
PYTHONPATH 中列出的目錄(類似環(huán)境變量 PATH ,由用戶定義,默認(rèn)為空);
site 模塊被 import 時添加的路徑1(site 會在運行時被自動 import)。
import site 所添加的路徑一般是 XXX/site-packages (Ubuntu 上是 XXX/dist-packages ),比如在我的機(jī)器上是 /usr/local/lib/python2.7/site-packages 。同時,通過 pip 安裝的包也是保存在這個目錄下的。如果懶得記 sys.path 的初始化過程,可以簡單的認(rèn)為 import 的查找順序是:
- 內(nèi)置模塊
.py 文件所在目錄
pip 或 easy_install 安裝的包
回到前面的問題,因為 import string 是通過搜尋 foo.py 文件所在目錄,找到 string.py 后 import 的,所以輸出取決于 import string.py 時執(zhí)行的代碼。
相對 import 與 絕對 import
相對 import
當(dāng)項目規(guī)模變大,代碼復(fù)雜度上升的時候,我們通常會把一個一個的 .py 文件組織成一個包,讓項目結(jié)構(gòu)更加清晰。這時候 import 又會出現(xiàn)一些問題,比如:一個典型包的目錄結(jié)構(gòu)是這樣的:
| string/
├── __init__.py
├── find.py
└── foo.py
|
如果 string/foo.py 的代碼如下:
| # string/foo.py
from string import find
print(find)
|
那么 python string/foo.py 的運行結(jié)果會是下面的哪一個呢?
<module 'string.find' from 'string/find.py'>
<function find at 0x123456789>
按我們前面講的各種規(guī)則來推導(dǎo),因為 foo.py 所在目錄 string/ 沒有 string 模塊(即 string.py ),所以 import 的是標(biāo)準(zhǔn)庫的 string ,答案是后者。不過,如果你把 foo 當(dāng)成 string 包中的模塊運行,即 python -m string.foo ,會發(fā)現(xiàn)運行結(jié)果是前者。同樣的語句,卻有著兩種不同的語義,這無疑加重了咱們的心智負(fù)擔(dān),總不能每次咱們調(diào)試包里的模塊時,都去檢查一下執(zhí)行的命令是 python string/foo.py 還是 python -m string.foo 吧?
相對 import 就是專為解決「包內(nèi)導(dǎo)入」(intra-package import)而出現(xiàn)的。它的使用也很簡單,from 的后面跟個 . 就行:
比如:
| # from string/ import find.py
from . import find
# from string/find.py import *
from .find import *
|
我們再看個復(fù)雜點的例子,有個包的目錄結(jié)構(gòu)長這樣:
| one/
├── __init__.py
├── foo.py
└── two/
├── __init__.py
├── bar.py
└── three/
├── __init__.py
├── dull.py
└── run.py
|
foo.py 、bar.py 、dull.py 中的代碼分別是 print(1) 、print(2) 、print(3) ,并且 run.py 的代碼如下:
| from . import dull
from .. import bar
from ... import foo
print('Go, go, go!')
|
我們通過 python -m one.two.three.run 運行 run.py ,可以看到 run.py 運行結(jié)果如下:
意思就是,from 后面出現(xiàn)幾個 . 就表示往上找第幾層的包。也可以將 run.py 改寫成下面這樣,運行結(jié)果是一樣的:
| from .dull import *
from ..bar import *
from ...foo import *
print('Go, go, go!')
|
好啦,相對 import 就介紹到這里,回到最初的問題。如果用相對 import,把 string/foo.py 改寫成:
| # string/foo.py
from . import find
print(find)
|
那么 python string/foo.py 和 python -m string.foo 的運行結(jié)果又是怎樣呢?運行一下發(fā)現(xiàn),兩者的輸出分別是:
| Traceback (most recent call last):
File "string/foo.py", line 1, in <module>
from . import find
ValueError: Attempted relative import in non-package
|
| <module 'string.find' from 'string/find.py'>
|
原因在于 python string/foo.py 把 foo.py 當(dāng)成一個單獨的腳本來運行,認(rèn)為 foo.py 不屬于任何包,所以此時相對 import 就會報錯。也就是說,無論命令行是怎么樣的,運行時 import 的語義都統(tǒng)一了,不會再出現(xiàn)運行結(jié)果不一致的情況。
絕對 import
絕對 import 和相對 import 很好區(qū)分,因為從行為上來看,絕對 import 會通過搜索 sys.path 來查找模塊;另一方面,除了相對 import 就只剩絕對 import 了嘛 :) 也就是說:
- 所有的
import ... 都是絕對 import
- 所有的
from XXX import ... 都是絕對 import
不過,第 2 點只對 2.7 及其以上的版本(包括 3.x)成立喔!如果是 2.7 以下的版本,得使用
| from __future__ import absolute_import
|
兩者的差異
首先,絕對 import 是 Python 默認(rèn)的 import 方式,其原因有兩點:
- 絕對 import 比相對 import 使用更頻繁
- 絕對 import 能實現(xiàn)相對 import 的所有功能
其次,兩者搜索模塊的方式不一樣:
- 對于相對 import,通過查看
__name__ 變量,在「包層級」(package hierarchy)中搜索
- 對于絕對 import,當(dāng)不處于包層級中時,搜索
sys.path
前面在介紹 sys.path 的初始化的時候,我在有個地方故意模棱兩可,即:
foo.py 所在目錄(如果是軟鏈接,那么是真正的 foo.py 所在目錄)或 當(dāng)前目錄
官方文檔的原文是:
the directory containing the input script (or the current directory).
這是因為當(dāng)模塊處于包層級中的時候,絕對 import 的行為比較蛋疼,官方的說法是:
The submodules often need to refer to each other. For example, the surround module might use the echo module. In fact, such references are so common that the import statement first looks in the containing package before looking in the standard module search path. Thus, the surround module can simply use import echo or from echo import echofilter. If the imported module is not found in the current package (the package of which the current module is a submodule), the import statement looks for a top-level module with the given name.
但是在我的測試中發(fā)現(xiàn),其行為可能是下面兩者中的任意一種:
比如,對于目錄結(jié)構(gòu)如下的包:
| father/
├── __init__.py
├── child/
│ ├── __init__.py
│ ├── foo.py
│ └── string.py
└── string/
└── __init__.py
|
其中,foo.py 代碼如下:
| import string
print(string)
|
import string 真正導(dǎo)入的模塊是:
version |
python -m child.foo |
python child/foo.py |
2.7.11 |
child/string.py |
child/string.py |
3.5.1 |
string/__init__.py |
child/string.py |
如果將 foo.py 的代碼改成(你可以 print(sys.path) 看看為什么改成這樣):
| import sys
sys.path[0] = ''
import string
print(string)
|
import 的模塊就變成了:
version |
python -m child.foo |
python child/foo.py |
2.7.11 |
child/string.py |
string/__init__.py |
3.5.1 |
string/__init__.py |
string/__init__.py |
為了避免踩到這種坑,咱們可以這樣子:
- 避免包或模塊重名,避免使用
__main__.py
- 包內(nèi)引用盡量使用相對 import
import 的大致過程
import 的實際過程十分復(fù)雜,不過其大致過程可以簡化為:
1
2
3
4
5
6
7
8
9
10
11
12 | def import(module_name):
if module_name in sys.modules:
return sys.modules[module_name]
else:
module_path = find(module_name)
if module_path:
module = load(module_path)
sys.modules[module_name] = module
return module
else:
raise ImportError
|
sys.modules 用于緩存,避免重復(fù) import 帶來的開銷;load 會將模塊執(zhí)行一次,類似于直接運行。
Tips
- import 會生成
.pyc 文件,.pyc 文件的執(zhí)行速度不比 .py 快,但是加載速度更快
- 重復(fù) import 只會執(zhí)行第一次 import
- 如果在
ipython 中 import 的模塊發(fā)生改動,需要通過 reload 函數(shù)重新加載
import * 會導(dǎo)入除了以 _ 開頭的所有變量,但是如果定義了 __all__ ,那么會導(dǎo)入 __all__ 中列出的東西
參考
-
官方說法是「Python 安裝時設(shè)定的默認(rèn)路徑」(The installation-dependent default path),而這玩意兒實際上是通過 site 模塊來設(shè)置的。 ?
|