当前位置: 首页 > 科技观察

PythonProbe实现原理

时间:2023-03-17 15:08:07 科技观察

关于Python的导入机制,之前写过一篇文章,写的很详细。感兴趣的可以点击这个链接查看:【深入探讨Python的导入机制:实现远程导入模块】另外,今天给大家推荐这篇文章,里面也介绍了Python的导入机制,以及与上条一起食用效果更佳。在本文中,我们将简要介绍一下Python探针的实现原理。同时,为了验证这个原理,我们还会实现一个简单的探测程序,一起统计指定函数的执行时间。probe的实现主要涉及以下知识点:sys.meta_pathsitecustomize.pysys.meta_pathsys.meta_path简单的说就是可以实现importhook的功能。当执行导入相关操作时,会触发sys.meta_path定义的对象列表。关于sys.meta_path更详细的信息,请参考python文档和PEP0302中sys.meta_path的相关内容。sys.meta_path中的对象需要实现一个find_module方法,这个find_module方法返回None或者一个实现了load_module方法的对象(代码可以从githubpart1_下载):importsysclassMetaPathFinder:deffind_module(self,fullname,path=None):print('find_module{}'.format(fullname))returnMetaPathLoader()classMetaPathLoader:defload_module(self,fullname):print('load_module{}'.format(fullname))sys.modules[fullname]=sysreturnsyssys.meta_path.insert(0,MetaPathFinder())if__name__=='__main__':importhttpprint(http)print(http.version_info)load_module方法返回一个模块对象,也就是import的模块对象。例如,我将http替换为上面的sys模块。$pythonmeta_path1.pyfind_modulehttpload_modulehttpsys.version_info(major=3,minor=5,micro=1,releaselevel='final',serial=0)我们可以通过sys.meta_path实现导入钩子的作用:当导入一个预定的模块时,这个模块中的对象会变成一只太子的狸猫,从而获取函数或方法的执行时间等检测信息。上面说了狸猫是给太子的,那么狸猫给太子的操作如何在一个对象上进行呢?对于函数对象,我们可以使用装饰器来替换函数对象(代码可以从githubpart2下载):importfunctoolsimporttimedeffunc_wrapper(func):@functools.wraps(func)defwrapper(*args,**kwargs):print('startfunc')start=time.time()result=func(*args,**kwargs)end=time.time()print('spent{}s'.format(end-start))returnresultreturnwrapperdefsleep(n):time.sleep(n)returnif__name__=='__main__':func=func_wrapper(sleep)print(func(3))执行结果:$pythonfunc_wrapper.pystartfuncspent3.004966974258423s3下面我们实现一个函数,计算指定模块指定函数的执行时间(代码可以从githubpart3下载)。假设我们的模块文件是hello.py:importtimedefsleep(n):time.sleep(n)return我们的导入钩子是hook.py:importfunctoolsimportimportlibimportsysimporttime_hook_modules={'hello'}classMetaPathFinder:deffind_module(self,fullname,path=None):print('find_module{}'.format(fullname))iffullnamein_hook_modules:returnMetaPathLoader()classMetaPathLoader:defload_module(self,fullname):print('load_module{}'.format(fullname))#``sys.modules``保存了什么是导入的moduleiffullnameinsys.modules:returnsys.modules[fullname]#先把自定义的finder从sys.meta_path中删除#防止下面执行import_module的时候再次触发这个finder#所以有递归调用的问题finder=sys.meta_path.pop(0)#importmodulemodule=importlib.import_module(fullname)module_hook(fullname,module)sys.meta_path.insert(0,finder)returnmodulesys.meta_path.insert(0,MetaPathFinder())defmodule_hook(fullname,module)):iffullname=='你好':module.sleep=func_wrapper(module.sleep)deffunc_wrapper(func):@functools.wraps(func)defwrapper(*args,**kwargs):print('startfunc')start=time.time()结果=func(*args,**kwargs)end=time.time()print('spent{}s'.format(end-start))returnresultreturnwrapper测试代码:>>>importhook>>>importhellofind_modulehelloload_modulehello>>>>>>hello.sleep(3)startfuncspent3.0029919147491455s3>>>其实上面的代码已经实现了探针的基本功能,但是有个问题就是上面的代码需要执行importhook操作来注册我们定义的hook。那么有没有办法在启动python解释器的时候自动执行importhook呢?答案是这个功能可以通过定义sitecustomize.py来实现。sitecustomize.py简单的说就是在python解释器初始化的时候,会自动导入存在于PYTHONPATH下的sitecustomize和usercustomize模块:实验项目的目录结构如下(代码可以从githubpart4下载):$tree.├──sitecustomize.py└──usercustomize.pysitecustomize.py:$catsitecustomize.pyprint('thisissitecustomize')usercustomize.py:$catusercustomize.pyprint('thisisusercustomize')将当前目录添加到PYTHONPATH,看效果:$exportPYTHONPATH=.$pythonthisissitecustomize<----thisisusercustomize<----Python3.5.1(default,Dec242015,17:20:27)[GCC4.2.1CompatibleAppleLLVM7.0.2(clang-700.1.81)]ondarwinType"help","copyright","credits"或者"license"获取更多信息。>>>可以看到确实是自动导入的。所以我们可以把之前的检测程序改成支持自动执行importhook(代码可以从githubpart5下载)。目录结构:$tree.├──hello.py├──hook.py├──sitecustomize.pysitecustomize.py:$catsitecustomize.pyimporthook结果:$exportPYTHONPATH=.$pythonfind_moduleusercustomizePython3.5.1(default,Dec242015,17:20:27)[GCC4.2.1CompatibleAppleLLVM7.0.2(clang-700.1.81)]ondarwinType"help","copyright","credits"or"license"formoreinformation.find_modulereadlinefind_moduleatexitfind_modulerlcompleter>>>>>>importhellofind_modulehelloload_modulehello>>>>>>hello.sleep(3)startfuncspent3.005002021789551s3但是上面的检测程序其实还有一个问题,就是需要手动修改PYTHONPATH。用过探针程序的朋友应该记得,使用newrelic等探针只需要执行一条命令:newrelic-adminrun-programpythonhello.py其实修改PYTHONPATH的操作是在程序newrelic-admin中完成的.接下来我们也要实现一个类似的命令行程序,暂且称之为agent.py。agent还是在之前程序的基础上修改的。先调整一个目录结构,把hook操作放在单独的目录下,这样设置PYTHONPATH后就不会有其他干扰了(代码可以从githubpart6下载)。$mkdirbootstrap$mvhook.pybootstrap/_hook.py$touchbootstrap/__init__.py$touchagent.py$tree.├──bootstrap│├──__init__.py│├──_hook.py│└──sitecustomize.py├──hello.py├──test.py├──agent.pybootstrap/sitecustomize.py内容修改为:$catbootstrap/sitecustomize.pyimport_hookagent.pyagent.py内容如下:importosimportsyscurrent_dir=os.path.dirname(os.path.realpath(__file__))boot_dir=os.path.join(current_dir,'bootstrap')defmain():args=sys.argv[1:]os.environ['PYTHONPATH']=boot_dir#执行如下python程序命令#sys.executable是python解释器程序的绝对路径``whichpython``#>>>sys.executable#'/usr/local/var/pyenv/versions/3.5.1/bin/python3.5'os.execl(sys.executable,sys.executable,*args)if__name__=='__main__':main()test.py的内容为:$cattest.pyimportsysimporthelloprint(sys.argv)print(hello.sleep(3))使用方法:$pythonagent.pytest.pyarg1arg2find_moduleusercustomizefind_modulehelloload_modulehello['test.py','arg1','arg2']startfuncspent3.005035161972046s3至此,我们已经实现了一个简单的python探测程序。当然,与实际的探测程序相比,肯定有很大的差距。本文主要讲解探针背后的实现原理。