這篇文章從另外一個不同的視角來分析一下Python的import機制,主要的目的是為了搞懂import中absolute、relative import遇到的幾個報錯。
這里不同的視角是指從Python import hooks這個方面來展開,當然本身關於Python import hooks有很多的文章,我這里不打算展開聊這個方面的內容,文章中主要會結合代碼和PEP 302 – New Import Hooks這個PEP。
1. 幾個跟import相關模塊屬性
首先我們需要了解幾個跟import相關的模塊屬性,因為后面我們分析代碼的時候會頻繁接觸到這些屬性,關於這些屬性詳細的介紹參考:import-related-module-attributes。
__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