写代码的时候路径很烦人!找不到指定路径?路径导入器了解一下?
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) # [ 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,target: None 这个基于路径的导入流程我们用一段代码来简单展示: def _get_spec(fullname,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,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中。 (编辑:李大同) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |