Python相對、絕對匯入淺析
這篇文章從另外一個不同的視角來分析一下Python的import機制,主要的目的是為了搞懂import中absolute、relative import遇到的幾個報錯。
這裡不同的視角是指從Python import hooks這個方面來展開,當然本身關於Python import hooks有很多的文章,我這裡不打算展開聊這個方面的內容,文章中主要會結合程式碼和PEP 302 – New Import Hooks這個PEP。
1. 幾個跟import相關模組屬性
首先我們需要了解幾個跟import相關的模組屬性,因為後面我們分析程式碼的時候會頻繁接觸到這些屬性,關於這些屬性詳細的介紹參考:
__name__:模組的全名稱,用來唯一的標識一個模組。
__package__:模組的__package__
屬性必須設定,而且必須是字串。當模組是一個包(package)的時候__package__
==__name__
,如果模組是非package並且是一個top-level模組那麼__package__
設定為空字串,對於子模組那麼__package__
就是上層父模組的__name__
。關於__package__
更詳細的內容可以參考:PEP 366 – Main module explicit relative imports
__path__:這個屬性就是用來區分一個模組是package還是py檔案,如果模組是package那麼
__path__
屬性就必須設定,但是這個屬性有可能沒有太多的其它意義。更詳細的__path__
內容參考:module.__path__。 2. Python import hooks的入門
雖然本文的重點不是關於Python import hooks,但是因為文章是從這個視角來闡述的,所以還是稍微介紹一點關於這個方面的一點入門知識點。
一般情況下我們在程式碼中使用
import foo
,那麼呼叫的其實是__builtin__.__import__
。有時候我們想要在程式碼中動態的載入某個模組那麼可以用imp
importlib
這兩個模組來實現,但是有時候我們想要更多的控制Python的import,比如要實現一個自動安裝、載入模組依賴的功能,那麼此時import hooks就能派上用場了。 Python提供了好兩種方式來做import hook:Meta hooks and Path hooks ,利用hook我們基本可以做到隨心所欲的import(當然有一些規則需要遵守的)。Python也提供了一個import hooks的模板,叫
ihooks
(/usr/lib/python2.7/ihooks.py),也即是我們後面要重點分析的一個模組。 如果想使用ihooks來代替預設的import功能,那麼在執行任何import之前執行如下程式碼即可:
#before any imports
import ihooks
ihooks.install()
這樣後面所有的import操作都會進入到ihooks.ModuleImporter.import_module()函式中。
3. 剖析ihooks,imports_module引數
執行完上面提到的ihooks.install()
以後import的入口變成了如下的import_module()
函式。
def import_module(self, name, globals=None, locals=None, fromlist=None,
level=-1):
parent = self.determine_parent(globals, level)
q, tail = self.find_head_package(parent, str(name))
m = self.load_tail(q, tail)
if not fromlist:
return q
if hasattr(m, "__path__"):
self.ensure_fromlist(m, fromlist)
return m
這個函式各個引數的具體含義可以參考builtin.__import__,重點說一下level
這個引數:
- 用來表示absolute還是relative匯入;
- 如果為0則表示是absolute匯入;
- 大於0表示relative匯入,相對匯入的父目錄的級數;
- -1意味著可能是absolute或relative匯入。
locals
引數暫時沒有用到。
4. 剖析ihooks,determine_parent()函式
def determine_parent(self, globals, level=-1):
if not globals or not level: #code 1
return None
pkgname = globals.get('__package__')
if pkgname is not None:
if not pkgname and level > 0: #code 2
raise ValueError, 'Attempted relative import in non-package'
else: #code 3
# __package__ not set, figure it out and set it
modname = globals.get('__name__')
if modname is None:
return None
if "__path__" in globals:
# __path__ is set so modname is already the package name
pkgname = modname
else:
# normal module, work out package name if any
if '.' not in modname:
if level > 0:
raise ValueError, ('Attempted relative import in '
'non-package')
globals['__package__'] = None
return None
pkgname = modname.rpartition('.')[0]
globals['__package__'] = pkgname
if level > 0: #code 4
dot = len(pkgname)
for x in range(level, 1, -1):
try:
dot = pkgname.rindex('.', 0, dot)
except ValueError:
raise ValueError('attempted relative import beyond '
'top-level package')
pkgname = pkgname[:dot]
try:
return sys.modules[pkgname] #code 5
except KeyError:
if level < 1:
warn("Parent module '%s' not found while handling "
"absolute import" % pkgname, RuntimeWarning, 1)
return None
else:
raise SystemError, ("Parent module '%s' not loaded, cannot "
"perform relative import" % pkgname)
determine_parent()
函式主要用來負責填充模組的__packge__
屬性、返回模組的錨點對應的模組(relative匯入才有)。
在程式碼中我對一些關鍵的程式碼分支做了code n這樣的標記,方便後面引用。
code 1:首先我們遇到的是code 1這個分支,globals
為空的情況我還沒有遇到過,但是level
為0的情況就是前面分析過的level
引數所示的情況:這是一個absolute匯入,比如你在匯入之前使用了from __future__ import absolute_import
,那麼level
就是為0。也就是說如果是absolute匯入那麼就無須再找出父模組,也不會再設定__package__
模組屬性,為什麼在這種情況下則不需要設定__package__
模組屬性呢?
讓我們好好的讀一讀這段話(來自:PEP 366 Proposed Change):
As with the current __name__ attribute, setting __package__ will be the responsibility of the PEP 302 loader used to import a module.
意思就是說設定__package__
是hooks中的loader(ModuleImporter包含了finder、loader)的責任,這個責任由determine_parent()
來完成。
When the import system encounters an explicit relative import in a module without __package__ set (or with it set to None ), it will calculate and store the correct value ( __name__.rpartition(‘.’)[0] for normal modules and __name__ for package initialisation modules).
這句話的意思是說如果遇到了明確的relative匯入並且__package__
未設定那麼loader會計算、儲存正確的的__package__
值。
從上面這兩條綜合來看就是說loader有責任設定__package__
,但是也是在某些條件的前提下才需要負責,對於我們code 1遇到的這種情況(不是明確的relative匯入),loader可以不用負這個責任。
code 2:這裡的ValueError: Attempted relative import in non-package
錯誤應該是Pythoner幾乎都遇到過的,但是別急,我們後面還會繼續遇到。這裡之所以會報錯就是因為__package__
為空字串則表示這是一個頂層的常規Python原始碼模組(top-level module),那麼此時如果再有relative匯入那麼就沒法進行模組的定位了。
code 3:這部分就是設定__package__
,整個的流程基本跟PEP 366 Proposed Change提到的一致,首先通過__path__
屬性來判斷這是一個package還是一個普通的原始碼模組,如果是package則直接設定__package__
為__name__
,否則通過__name__.rpartition('.')[0]
計算得到。在這裡我們又一次遇到了前面的ValueError
,這裡報錯的原因跟前面大同小異,不再過多的解釋。
至此我們完成了determine_parent()
的第一個重要功能:設定模組的__package__
屬性。
code 4:如果是relative匯入,那麼需要計算相對的錨點是哪個,例如在spam.foo.test模組中執行import ..sub
那麼最後計算得出需要匯入的模組是spam.sub。
在這個部分我們遇到了另外一個常見的錯誤ValueError: attempted relative import beyond top-level package
,這個錯誤的原因就是我們在計算錨點的時候超過了最高模組,例如在spam.foo.test模組中執行import ...sub
。
code 5:完成了最後一個功能:返回錨點模組。
5. 剖析ihooks,find_head_package()函式
def find_head_package(self, parent, name):
if '.' in name:
i = name.find('.')
head = name[:i]
tail = name[i+1:]
else:
head = name
tail = ""
if parent:
qname = "%s.%s" % (parent.__name__, head)
else:
qname = head
q = self.import_it(head, qname, parent) #code 1
if q: return q, tail
if parent:
qname = head
parent = None
q = self.import_it(head, qname, parent) #code 2
if q: return q, tail
raise ImportError, "No module named '%s'" % qname
從函式名我們就能大概猜到這個函式的作用,就是匯入完整模組路徑名中的第一個模組,類似就是如果我們要匯入spam.foo.test
,那麼這個函式是先匯入spam
模組。
這個函式的理論我們從PEP-0302 Specification part 1: The Importer Protocol的第三段話中可以看到,大致的意思就是我們先做relative匯入,
例如我們在spam
中執行import foo
,那麼會要先嚐試匯入spam.foo
(我們上面程式碼中標註的code 1),如果失敗了則再執行absolute匯入foo
(我們上面程式碼中標註的code 2)。
6. 剖析ihooks,load_tail()函式
前面我們把第一個模組已經匯入了那麼接下來就是把剩下的(尾部)的模組匯入了,這就是這個函式的功能。程式碼就不貼了,比較簡單,就是迴圈把完整模組名中的每一個子模組匯入,函式的理論可以參考PEP-0302 Specification part 1: The Importer Protocol的第四段話。
7. 剖析ihooks,ensure_fromlist()函式
這個函式就是把類似from spam import foo.test
中foo.test
部分匯入。
8. 剖析ihooks,import_it()函式
def import_it(self, partname, fqname, parent, force_load=0):
if not partname:
# completely empty module name should only happen in
# 'from . import' or __import__("")
return parent
if not force_load:
try:
return self.modules[fqname] #code 1
except KeyError:
pass
try:
path = parent and parent.__path__
except AttributeError:
return None
partname = str(partname)
stuff = self.loader.find_module(partname, path) #code 2
if not stuff:
return None
fqname = str(fqname)
m = self.loader.load_module(fqname, stuff) #code 3
if parent:
setattr(parent, partname, m)
return m
這個函式是執行匯入的核心函式,前面我們介紹的各種函式都是最終通過import_it()
來執行最後的匯入。
函式程式碼其實也挺簡單的,特別是你能結合PEP-0302 Specification part 1: The Importer Protocol來看程式碼。
code 1:如果cache中已經存在該模組,那麼直接返回該模組。
code 2:查詢對應的模組,返回一個三元組,間接呼叫的imp.find_module
。關於這個函式更多的內容除了上面的”PEP-0302 Specification part 1: The Importer Protocol”以外還可以參考imp.find_module。
code 3:載入對應的模組,就是呼叫imp
內的各種函式,不再贅述。
整個import_module()
函式介紹完成了,在閱讀ihooks.py
或者Python/import.c
原始碼之前建議各位先把幾個PEP以及Python Language Reference的幾篇文章先通讀一遍,如果有些你暫時沒弄清楚的那麼就可以留到原始碼中去弄清楚。
- The import system
- PEP 302 – New Import Hooks
- imp — Access the import internals
- PEP 366 – Main module explicit relative imports