1. 程式人生 > >寫程式碼的時候路徑很煩人!找不到指定路徑?路徑匯入器瞭解一下?

寫程式碼的時候路徑很煩人!找不到指定路徑?路徑匯入器瞭解一下?

path

entry finders

我們知道在sys.meta_path中預設存在三種Finder:BuiltinImporter,FrozenImporter和PathFinder。其中第三種就是預設的path entry finder。它的作用是完成所有基於路徑的匯入工作。所有的對某個路徑下的包或模組的匯入(例如絕對匯入和顯式相對匯入,甚至zipimport),都是由PathFinder完成的。需要強調的是,這裡所說的路徑可以是檔案系統中的目錄,也可以是一個URL,也可以是一個壓縮包,甚至可以是資料庫等。由於它也是一個Finder,那麼它就存在方法find_spec用於尋找目標模組的spec物件。我們嘗試從sys.meta_path中刪除這個Finder,那麼匯入自定義的模組就無法工作了:

# .
# ├── a.py
# └── b.py
# b.py
import sys
from pprint import pprint
pprint(sys.meta_path)
# [<class '_frozen_importlib.BuiltinImporter'>,
# <class '_frozen_importlib.FrozenImporter'>,
# <class '_frozen_importlib_external.PathFinder'>]
# 嘗試刪除PathFinder
sys.meta_path.pop(-1)
import a
# ModuleNotFoundError: No module named 'a'

path entry finders究竟是怎麼工作的呢?

進群:548377875  即可獲取數十套PDF哦!

當我們匯入一個基於路徑的模組時(例如一個自定義的模組),Python會從sys.meta_path找到PathFinder來處理。PathFinder會在一個指定的路徑列表中搜索finder,這個路徑列表可能是sys.path,也可能是包的__path__屬性。這裡,Python利用了快取的機制來加快搜索速度。因為某一個路徑可能會被多次搜尋到,Python會將路徑與finder的對應關係快取至sys.path_importer_cache中,這樣,下次搜尋相同路徑就會直接呼叫快取中的finder獲取spec

。如果快取中不存在路徑的finder,則會利用sys.path_hooks中的函式來嘗試建立finder,並將路徑作為引數傳入函式中,否則快取一個None表明該路徑無法建立finde

整個流程略顯複雜,其核心是三個變數的關係:sys.path,sys.path_importer_cache和sys.path_hooks。sys.path儲存著搜尋模組的路徑,而sys.path_importer_cache則快取著上述路徑所對應的finders,最後,sys.path_hooks儲存著用於從指定路徑返回finder的可呼叫物件。

我們通過下面一個栗子來展示一下上述流程。首先我們定義一個Finder類,與之前不同的是,Finder類需要一個初始化方法接收一個path引數。find_spec是必須的。之後,我們將Finder類加入sys.path_hooks中,來看看其呼叫流程:

# 目錄namespace下
import sys
from pprint import pprint
import importlib.util, importlib.machinery
class PathFinder:
 def __init__(self, path):
 print('Initial path {} for PathFinder'.format(path))
 self._path = path
 def find_spec(self, fullname, path=None, target=None):
 if path is None:
 path = self._path
 print('fullname: {}, path: {}, target: {}'.format(fullname, path, target))
 return importlib.machinery.ModuleSpec(fullname, loader=self)
 def create_module(self, fullname):
 return None
 def exec_module(self, module):
 return None
sys.path_importer_cache.clear()
sys.path_hooks.insert(0, PathFinder)
import a
# Initial path ~/namespace for PathFinder
# fullname: a, path: ~/namespace, target: None
pprint(sys.path_importer_cache)
# {'~/namespace': <__main__.PathFinder object at 0x7f2c954cc400>}

我們在sys.path_hooks中插入了PathFinder類之後,匯入一個基於路徑的模組就會呼叫PathFinder(path)產生一個finder物件,之後一方面會將該物件快取進sys.path_importer_cache中,另一方面,Python會呼叫該物件的find_spec方法以及create_module和exec_module來匯入模組。由於sys.path_importer_cache的作用,下一次該路徑下的模組匯入就不再建立新的物件了:

import b
# fullname: a, path: ~/namespace, target: None

這個基於路徑的匯入流程我們用一段程式碼來簡單展示:

def _get_spec(fullname, path, target=None):
 if path is None:
 path = sys.path
 for entry in path:
 try:
 finder = sys.path_importer_cache[entry]
 except KeyError:
 for hook in sys.path_hooks:
 try:
 finder = hook(entry)
 except ImportError:
 continue
 else:
 finder = None
 sys.path_importer_cache[entry] = finder
 if finder is not None:
 spec = finder.find_spec(fullname, target)
 if spec is None:
 continue
 return spec

上述程式碼簡單展示了Python中path entry finders的內部機制,當然其中略去了很多細節,僅供理解。

二種“鉤”

path_hooks有什麼實際的用處嗎?最常見的用法是代替meta_path作為匯入鉤的另一種實現方式。我們可以將自定義的鉤函式放進path_hooks中,在匯入前做一些個性化工作。下面舉個例子:

匯入配置檔案

通常情況下,我們想要匯入一個配置檔案,需要開啟該檔案,並依照某一格式來解析配置內容。這裡我們嘗試利用路徑匯入鉤來實現配置檔案的匯入功能。我們假定配置檔案均位於config目錄下(時刻注意path entry finder是針對路徑層級的finder),我們來嘗試構建一個用於匯入config中JSON格式的配置檔案的path entry finder:

import json
import types
import pathlib
import importlib.machinery
class JSONImporter:
 @classmethod
 def hook(cls, path):
 '''這裡定義用於放入path_hooks的鉤函式,只處理config目錄'''
 if path.endswith('config'):
 return cls(path) # 如果是config目錄,則例項化一個物件。
 def __init__(self, path):
 self._path = pathlib.Path(path)
 self._allconfig = self._path.glob('*.json') 
 # glob用於遍歷出所有.json格式的檔案
 def find_spec(self, fullname, path=None, target=None):
 fullpath = self._path / pathlib.Path(fullname + '.json')
 if fullpath in self._allconfig:
 spec = importlib.machinery.ModuleSpec(fullname, self, is_package=True)
 spec.origin = fullpath # 這裡為了後續載入便利
 return spec
 else:
 return None
 def create_module(self, spec):
 module = types.ModuleType(spec.name)
 module.__file__ = spec.origin # spec物件和module物件對同一個內容的屬性名不同
 return module
 def exec_module(self, module):
 try:
 config = json.loads(module.__file__.read_text()) # 這裡是實際載入語句
 module.__dict__.update(config)
 except FileNotFoundError:
 raise ImportError('No module named '{}'.'.format(module.__name__)) from None
 except json.JSONDecodeError:
 raise ImportError('Unable to load JSON, object format is corrupted, file: {}'.format(module.__name__)) from None
import sys
sys.path_hooks.insert(0, JSONImporter.hook) # 需要插入到path_hooks的第一項
sys.path.append('./config') # 需要將路徑加入sys.path
# sys.path_importer_cache.pop('./config') 要保證cache中沒有config路徑

上面我們構建了一個匯入配置的Importer,下面來看看如何使用它,下面是development.json配置檔案的內容:

{
 "host": "localhost",
 "port": "8080",
 "auth": {
 "username": "Python",
 "password": "****"
 }
}

下面是main.py檔案的內容:

# 目錄結構
# .
# ├── config
# │ └── development.json
# ├── configimporter.py
# └── main.py
# main.py
import configimporter
import development as dev
print(dev.host)
# localhost
print(dev.auth)
# {'username': 'Python', 'password': '********'}
from development import port
print(port)
# 8080

更多的例子可以參考Python Cookbook中給出的兩個較複雜的例子,一個是匯入一個URL路徑,另一個是在匯入模組前修改模組內容。

VS

meta path finders

path entry finders和meta path finders有什麼區別呢?雖然二者的工作流程幾乎相同,但是它們還是有著細微的差別,path entry finders主要用於處理路徑級別的匯入,簡單來說,一個路徑對應一個path entry finders;而meta path finders則用於對於特定型別模組的自定義匯入方式。想要自定義path entry finders,需要插入到sys.path_hooks變數中,並保證目標路徑位於sys.path中,且sys.path_importer_cache中沒有快取;而想要自定義meta path finders,則需要插入到sys.meta_path中。