寫程式碼的時候路徑很煩人!找不到指定路徑?路徑匯入器瞭解一下?
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
整個流程略顯複雜,其核心是三個變數的關係: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中。