最近遇到一个有意思的需求,需要重载 exit 和 sys.exit 这两个函数,希望用户在调用这两个函数时,能有一些操作记录,方便判断;但是又不能直接更改 c 源码后重新编译。
第一个方式是这样实现:
import sys
temp = exit
def myExit(code):
print("get code to exit ",code)
temp(code)
exit = myExit
sys.exit = myExit
print("hehe")
exit(1) # sys.exit(1)
初看上面这个没有什么问题,但是它无法解决跨包的问题。假设上面的代码在包 A 中,A 会调用 B 包的接口,而 B 包在必要条件下,会调用 exit 或者 sys.exit 退出进程。如果按照上面的方式来修改,除非在每个包中都重载 exit 或者 sys.exit,否则会造成循环引用。当然还有一个办法是建立一个全局的重载包,在每个独立库中都 import 这个包中的内容;不过一方面这个改动几乎很大,很多包是第三方的,不方便这么操作,另一方面是它只能解决 sys.exit 却不能解决 exit 的重载。而且这个重载一定要在每个包的开头导入,否则可能还没来得及重载退出函数,进程就退出了。
目前找到一个比较有效的方法,不过这需要两个前置条件:
- python 在执行具体的入口文件前,可以挂钩子;
- python 有自己作用域分层结构,很多直接使用的函数在内建模块中;
python 进程启动前的钩子函数
sitecustomize.py,这是一个独特的文件,一般放在 site-packages 下边,python 每次启动时,在执行指定的脚本前,会先查看有没有这个文件,如果有,就先执行这里边的内容。所以我们可以把相关重载的逻辑放在这个文件中;但是如果按照上面的方式重载 exit ,还是不行,因为此时 exit 只是个文件内的局部变量。需要更改内建级别的才可以。
更改内置函数
我们使用的 float、int、abs 等函数,都没有加模块名,因为这些是内置模块,都在 builtins 中;但是 builtins 这个又有包可以被导入增删查改等。比如我们完全可以像下面这样更改 abs 的定义:
import builtins
def myAbs(num):
return num - 1
builtins.abs = myAbs
按照上面的方式,则 abs 的含义完全变了。
结合上面两点,我们就可以在不重新编译 python 的情况下,实现 exit 和 sys.exit 的重载了,只要在 site-packages 文件夹下新建 sitecustomize.py 文件,并在其中写入以下代码即可:
import builtins
import sys
temp = builtins.exit
def myExit(code):
print("get code to exit ",code)
temp(code)
builtins.exit = myExit
sys.exit = myExit
此时,整个进程的 exit 和 sys.exit 就全部变成了自己想要的。不得不说,python 中这种基于指针/引用的方式还是非常有趣的。