1. 程式人生 > >Python相對、絕對匯入淺析

Python相對、絕對匯入淺析

這篇文章從另外一個不同的視角來分析一下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.testfoo.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