本文呢,将简单讲述一下 Python 探针的实现原理。同时为了验证这个原理,我们也会一起来实现一个简单的统计指定函数执行时间的探针程序。

探针的实现主要涉及以下几个知识点:

  • sys.meta_path
  • sitecustomize.py

sys.meta_path

sys.meta_path 这个简单的来说就是可以实现 import hook 的功能, 当执行 import 相关的操作时,会触发 sys.meta_path 列表中定义的对象。关于 sys.meta_path 更详细的资料请查阅 python 文档中 sys.meta_path 相关内容以及 PEP 0302 。

sys.meta_path 中的对象需要实现一个 find_module 方法, 这个 find_module 方法返回 None或一个实现了 load_module 方法的对象 (代码可以从 github 上下载 part1_) :

import sysclass MetaPathFinder:def find_module(self, fullname, path=None):print('find_module {}'.format(fullname))return MetaPathLoader()class MetaPathLoader:def load_module(self, fullname):print('load_module {}'.format(fullname))sys.modules[fullname] = sysreturn syssys.meta_path.insert(0, MetaPathFinder())if __name__ == '__main__':import httpprint(http)print(http.version_info)

load_module 方法返回一个 module 对象,这个对象就是 import 的 module 对象了。比如我上面那样就把 http 替换为 sys 这个 module 了。

$ python meta_path1.py
find_module http
load_module http
<module 'sys' (built-in)>
sys.version_info(major=3, minor=5, micro=1, releaselevel='final', serial=0)

通过 sys.meta_path 我们就可以实现 import hook 的功能:当 import 预定的 module 时,对这个 module 里的对象来个狸猫换太子, 从而实现获取函数或方法的执行时间等探测信息。

上面说到了狸猫换太子,那么怎么对一个对象进行狸猫换太子的操作呢?对于函数对象,我们可以使用装饰器的方式来替换函数对象(代码可以从 github 上下载 part2) :

import functools
import timedef func_wrapper(func):@functools.wraps(func)def wrapper(*args, **kwargs):print('start func')start = time.time()result = func(*args, **kwargs)end = time.time()print('spent {}s'.format(end - start))return resultreturn wrapperdef sleep(n):time.sleep(n)return nif __name__ == '__main__':func = func_wrapper(sleep)print(func(3))

执行结果:

$ python func_wrapper.py
start func
spent 3.004966974258423s
3

下面我们来实现一个计算指定模块的指定函数的执行时间的功能(代码可以从 github 上下载 part3) 。

假设我们的模块文件是 hello.py:

import timedef sleep(n):time.sleep(n)return n

我们的 import hook 是 hook.py:

import functools
import importlib
import sys
import time_hook_modules = {'hello'}class MetaPathFinder:def find_module(self, fullname, path=None):print('find_module {}'.format(fullname))if fullname in _hook_modules:return MetaPathLoader()class MetaPathLoader:def load_module(self, fullname):print('load_module {}'.format(fullname))# ``sys.modules`` 中保存的是已经导入过的 moduleif fullname in sys.modules:return sys.modules[fullname]# 先从 sys.meta_path 中删除自定义的 finder# 防止下面执行 import_module 的时候再次触发此 finder# 从而出现递归调用的问题finder = sys.meta_path.pop(0)# 导入 modulemodule = importlib.import_module(fullname)module_hook(fullname, module)sys.meta_path.insert(0, finder)return modulesys.meta_path.insert(0, MetaPathFinder())def module_hook(fullname, module):if fullname == 'hello':module.sleep = func_wrapper(module.sleep)def func_wrapper(func):@functools.wraps(func)def wrapper(*args, **kwargs):print('start func')start = time.time()result = func(*args, **kwargs)end = time.time()print('spent {}s'.format(end - start))return resultreturn wrapper

测试代码:

>>> import hook
>>> import hello
find_module hello
load_module hello
>>>
>>> hello.sleep(3)
start func
spent 3.0029919147491455s
3
>>>

其实上面的代码已经实现了探针的基本功能。不过有一个问题就是上面的代码需要显示的 执行 import hook 操作才会注册上我们定义的 hook。

那么有没有办法在启动 python 解释器的时候自动执行 import hook 的操作呢?答案就是可以通过定义 sitecustomize.py 的方式来实现这个功能。

sitecustomize.py

简单的说就是,python 解释器初始化的时候会自动 import PYTHONPATH 下存在的 sitecustomize 和 usercustomize 模块:

实验项目的目录结构如下(代码可以从 github 上下载 part4) :

$ tree
.
├── sitecustomize.py
└── usercustomize.py

sitecustomize.py:

$ cat sitecustomize.py
print('this is sitecustomize')

usercustomize.py:

$ cat usercustomize.py
print('this is usercustomize')

把当前目录加到 PYTHONPATH 中,然后看看效果:

$ export PYTHONPATH=.
$ python
this is sitecustomize       <----
this is usercustomize       <----
Python 3.5.1 (default, Dec 24 2015, 17:20:27)
[GCC 4.2.1 Compatible Apple LLVM 7.0.2 (clang-700.1.81)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>>

可以看到确实自动导入了。所以我们可以把之前的探测程序改为支持自动执行 import hook (代码可以从 github 上下载 part5) 。

目录结构:

$ tree
.
├── hello.py
├── hook.py
├── sitecustomize.py

sitecustomize.py:

$ cat sitecustomize.py
import hook

结果:

$ export PYTHONPATH=.
$ python
find_module usercustomize
Python 3.5.1 (default, Dec 24 2015, 17:20:27)
[GCC 4.2.1 Compatible Apple LLVM 7.0.2 (clang-700.1.81)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
find_module readline
find_module atexit
find_module rlcompleter
>>>
>>> import hello
find_module hello
load_module hello
>>>
>>> hello.sleep(3)
start func
spent 3.005002021789551s
3

不过上面的探测程序其实还有一个问题,那就是需要手动修改 PYTHONPATH 。用过探针程序的朋友应该会记得, 使用 newrelic 之类的探针只需要执行一条命令就 可以了: newrelic-admin run-program python hello.py 实际上修改 PYTHONPATH 的操作是在 newrelic-admin 这个程序里完成的。

下面我们也要来实现一个类似的命令行程序,就叫 agent.py 吧。

agent

还是在上一个程序的基础上修改。先调整一个目录结构,把 hook 操作放到一个单独的目录下, 方便设置 PYTHONPATH 后不会有其他的干扰(代码可以从 github 上下载 part6 )。

$ mkdir bootstrap
$ mv hook.py bootstrap/_hook.py
$ touch bootstrap/__init__.py
$ touch agent.py
$ tree
.
├── bootstrap
│   ├── __init__.py
│   ├── _hook.py
│   └── sitecustomize.py
├── hello.py
├── test.py
├── agent.py

bootstrap/sitecustomize.py 的内容修改为:

$ cat bootstrap/sitecustomize.py
import _hook

agent.py 的内容如下:

import os
import syscurrent_dir = os.path.dirname(os.path.realpath(__file__))
boot_dir = os.path.join(current_dir, 'bootstrap')def main():args = sys.argv[1:]os.environ['PYTHONPATH'] = boot_dir# 执行后面的 python 程序命令# sys.executable 是 python 解释器程序的绝对路径 ``which python``# >>> 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 的内容为:

$ cat test.py
import sys
import helloprint(sys.argv)
print(hello.sleep(3))

使用方法:

$ python agent.py test.py arg1 arg2
find_module usercustomize
find_module hello
load_module hello
['test.py', 'arg1', 'arg2']
start func
spent 3.005035161972046s
3

至此,我们就实现了一个简单的 python 探针程序。当然,跟实际使用的探针程序相比肯定是有 很大的差距的,这篇文章主要是讲解一下探针背后的实现原理。

完整代码加群:739021630

非常干货:Python 探针实现原理相关推荐

  1. python find函数实现原理_非常干货:Python 探针实现原理

    △点击上方"Python猫"关注 ,回复" 1 "领取电子书 剧照 | <棋魂> 原文:https://segmentfault.com/a/119 ...

  2. python QTreeWidgetItem下面有几个子tree_非常干货:Python 探针实现原理

    ↑↑↑关注后"星标"简说Python 人人都可以简单入门Python.爬虫.数据分析 简说Python推荐 来源:https://segmentfault.com/a/119000 ...

  3. python 底层原理_Python 探针实现原理

    本文将简单讲述一下 Python 探针的实现原理. 同时为了验证这个原理,我们也会一起来实现一个简单的统计指定函数执行时间的探针程序. 探针的实现主要涉及以下几个知识点: sys.meta_path ...

  4. python程序的原理_Python程序的执行原理(转)

    1. 过程概述 Python先把代码(.py文件)编译成字节码,交给字节码虚拟机,然后虚拟机一条一条执行字节码指令,从而完成程序的执行. 2. 字节码 字节码在Python虚拟机程序里对应的是PyCo ...

  5. python装饰器原理-Python装饰器原理与用法分析

    这篇文章主要介绍了Python装饰器原理与用法,结合实例形式分析了Python装饰器的概念.原理.使用方法及相关操作注意事项,需要的朋友可以参考下 本文实例讲述了Python装饰器原理与用法.分享给大 ...

  6. python装饰器原理-简单了解python装饰器原理及使用方法

    这篇文章主要介绍了简单了解python装饰器原理及使用方法,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下 如果你接触 Python 有一段时间了的话 ...

  7. python装饰器原理-python装饰器原理与用法深入详解

    本文实例讲述了python装饰器原理与用法.分享给大家供大家参考,具体如下: 你会Python嘛? 我会! 那你给我讲下Python装饰器吧! Python装饰器啊?我没用过哎 以上是我一个哥们面试时 ...

  8. python装饰器实例-Python装饰器原理与简单用法实例分析

    本文实例讲述了Python装饰器原理与简单用法.分享给大家供大家参考,具体如下: 今天整理装饰器,内嵌的装饰器.让装饰器带参数等多种形式,非常复杂,让人头疼不已.但是突然间发现了装饰器的奥秘,原来如此 ...

  9. Python 装饰器原理和基本实现

    Python 装饰器原理和基本实现 http://www.cnblogs.com/wupeiqi/articles/4980620.html http://www.cnblogs.com/vamei ...

最新文章

  1. c#中邮件收发处理(POP3,IMAP,SMTP)的实现方法
  2. 【读书笔记】iOS-属性列表
  3. 配置_DruidDataSource参考配置
  4. 做一个.net 程序员要掌握的知识提纲
  5. 小波滤波器与其他滤波器的区别_滤波器国产 VS 国外
  6. 开源作者在行动:疫情防控相关开源项目推荐
  7. 数组随机排序(随手记)
  8. oracle安装检测空间china,oracle安装 - Ginn的个人空间 - OSCHINA - 中文开源技术交流社区...
  9. 查看Linux磁盘文件占用大小
  10. java8堆内存模型_「GC系列」JVM堆内存分代模型及常见的垃圾回收器
  11. cdecl.org 翻译C声明的网站
  12. Python身份证号码识别
  13. 中铁置业引入USB Server助力RPA机器人
  14. 企业微信双开及三开的方法
  15. 2019新版c智播客h马程序员H5全栈工程师培训项目实战
  16. linux如何破解密码
  17. 三星手机服务器无影响,终于找到手机网速慢的原因了!原来有这么多讲究
  18. javascript 去掉html标签,js怎么去掉html标签
  19. Mac系统随笔 | (2) Macbook WiFi共享
  20. EXCEL应用:数据可视化终极教程

热门文章

  1. CSI数据dat文件转换mat文件问题(matlab、python数据处理)
  2. QGis、Qt对话框上的OK、Open、Cancel、Help等英文翻译
  3. TypeError: func() missing 1 required positional argument: 'XXXXX' 报错原因
  4. Axure基础:元件库与基础原件
  5. 机器学习理论_吃瓜系列1:基本概念
  6. 【娱乐】exe逆向工程(破译《游戏开发》老师的成绩程序)
  7. oracle metalink获取,Oracle的MetaLink使用
  8. DOS命令---定时关机或重启
  9. 迅雷,qq旋风等下载链接加密算法解析
  10. 读龙应台《目送》有感