利用python ast包,绘制python代码的调用关系图(可分析互相调用的多个py文件)

我的目的是辅助代码阅读(也方便写文档),因此不需要太详细的信息,只需要看用户定义的函数的函数调用关系。

1.开源项目staticfg的安装和直接使用

一开始我试图寻找现成的轮子,在github上得偿所愿,https://github.com/coetaur0/staticfg,需要详细函数调用图的朋友可以一试,我总结了staticfg安装使用中的问题:
我的电脑上装了anaconda和pycharm,pycharm的解释器可以设置成anaconda中的python,这样就可以在pycharm中使用anaconda装的包。
在anaconda navigator中打开cmd.exe,可以直接使用pip命令来安装staticfg。pip会先安装astor库,如果超时的话就等它耗尽超时次数,然后重试。
pip成功安装之后,尝试readme中的demo,会报错,大意是说要将graphviz的可执行文件添加到环境变量。需要到http://www.graphviz.org/download/去下载,安装时可以选择自动添加到环境变量。然后readme中的斐波那契demo就可以正常分析了。
如果分析的py文件中有中文注释,会报unicode decode error,说gbk无法解码某一个byte。据说是因为python默认使用gbk。其中一个解决方法是在代码开头增加如下代码:

import _locale
_locale._getdefaultlocale = (lambda *args: ['zh_CN', 'utf8'])

不用修改任何代码的另一个临时解决方法是,python -X utf8 build_cfg.py [input] [output]。其中,build_cfg.py在clone下来的example目录下。
之后还可能会有另一个错误,在builder.py里,大意是+=的运算数不能是str和nonetype。这个问题貌似是输入文件中有strip函数引起的。如果可以顺利执行的话,生成的结果是一字排开的,如果函数很多,观感很差。函数内的很多语句都会被放在图里,真要读图感觉还没直接看代码快。

2.绘制python的简单调用关系图(支持多文件)

首先,我总结一下staticfg中不尽人意的地方:

  1. 代码中有中文注释时,可能会报错。(上面提到了解决方法,我在自己的实现中已经内置了,不会有问题。)
  2. 可能会出现上文提到的nonetype错误。(我改为了输出warnings,避免报错中止。)
  3. 生成的结果中,有一些一字排开的孤立结点(staticfg中对graphviz的使用比较粗糙,解决见下文)
  4. 结果中有大量函数内语句(比如return语句)和系统内置函数(我不需要这些信息)

为了解决这些问题,我借鉴了staticfg,做了自己的实现。

2.1 python ast前置知识(可酌情跳过)

了解前置知识之后,大致可以读懂staticfg的源码,并进行自己的改写。
python官方文档推荐的 https://greentreesnakes.readthedocs.io/
但这个我上不了,所以只好找了一些国内博客,也能解决问题:https://blog.csdn.net/ma89481508/article/details/56017697

print type(node).__name__

注意上面这篇博客中的这句话,可以在没有文档的情况下获取你想知道的node类型(比如你想知道a=10属于哪种类型,就输入一个只有a=10的py文件),从而知道想要override的官方函数的名字。(更笨的办法是慢慢翻ide的代码提示)

此外,这篇博客也很有帮助 https://www.cnblogs.com/yssjun/p/10069199.html

2.2 代码实现

import os
import ast
import sys
from graphviz import Digraph
import _locale
_locale._getdefaultlocale = (lambda *args: ['zh_CN', 'utf8'])


def simplecfg(*args):
    visitor = CodeVisitor()
    for infile in args[0]:
        f = open(infile, "r")
        r_node = ast.parse(f.read())
        f.close()
        visitor.filename = os.path.basename(infile).split('.')[0]
        visitor.visit(r_node)

    fpos = {}#fpos存放函数basename所在的py文件
    for func in visitor.userfunc:
        fr = func.split('.')[0]
        bk = func.split('.')[-1]
        fpos[bk] = fr

    dest = {}#dest存放每个userfunc下调用了哪些userfunc
    for line in visitor.info:
        if line.startswith('User Function Name'):
            defnow = line.split(':')[1]
            dest[defnow] = []
            continue
        for func in visitor.userfunc:
            basename = func.split('.')[-1]
            line_tail = line.split(':')[-1]
            line_tail = line_tail.split('.')[-1]
            if basename == line_tail:
                dest[defnow].append(basename)
                break

    dot = Digraph(comment='The Round Table')

    ctr = 0
    alias = {}
    #在dot语法中,结点有自己的名字,这个名字跟结点在图片上显示的函数名字不同。alias存储两者的映射。

    for func in visitor.userfunc:
        ctr += 1
        alias[func] = 'A'+str(ctr)#跟dot语法的命名规则有关,也可以用其它命名,不必纠结
        dot.node(alias[func], func)
    for key in dest.keys():
        for dst in dest[key]:
            fullname = fpos[dst] + '.' + dst
            dot.edge(alias[key], alias[fullname])

    dot.render('test-output/round-table.gv')
    # print(ast.dump(r_node))


class CodeVisitor(ast.NodeVisitor):
    userfunc = []
    info = []
    filename = ''
    def generic_visit(self, node):
        # print(type(node).__name__)
        ast.NodeVisitor.generic_visit(self, node)
    def visit_FunctionDef(self, node):
        # print('User Function Name:%s' % node.name)
        self.info.append('User Function Name:'+self.filename+'.'+node.name)
        self.userfunc.append(self.filename+'.'+node.name)
        ast.NodeVisitor.generic_visit(self, node)
    def visit_Call(self, node):
        # print(node._fields)
        def recur_visit(node):
            if type(node) == ast.Name:
                return node.id
            elif type(node) == ast.Attribute:
                # Recursion on series of calls to attributes.
                # print(node.attr)
                func_name = recur_visit(node.value)
                if type(node.attr) == str and type(func_name) == str:
                    func_name += '.' + node.attr
                # else:
                    # print('attention!!!', type(node.attr), type(func_name))
                return func_name
            elif type(node) == ast.Str:
                return node.s
            elif type(node) == ast.Subscript:
                return node.value.id

        func = node.func
        # print(type(func), func._fields)
        func_name = recur_visit(func)
        if(type(func_name)==str):
            self.info.append('\tUser function Call:'+self.filename+'.'+func_name)
        ast.NodeVisitor.generic_visit(self, node)


simplecfg(sys.argv[1:])

2.3 代码的使用

在使用上面的代码之前,需要正确安装graphviz,可参考第一部分。
如果用pycharm的话,可以在下方的terminal中用命令行运行上面的py文件,命令如下:
python 上面的py文件.py 输入文件1.py 输入文件2.py 输入文件n.py
执行过后,在上面的py文件的存储位置找test-output目录,可以看见生成了gv文件和pdf文件。
可以先直接打开pdf文件看一下,但我估计你不会满意,因为作的图太宽,根本看不清。
在stackoverflow上获得了如下解决方法:

unflatten -c 5 round-table.gv | dot -Gratio="fill" -Gsize="20,5" -Tpdf -o round-table.gv.pdf

在cmd或者powershell中先进入gv文件的目录,然后执行这条命令(前提是graphviz已经正确安装,参考第一部分),生成的pdf即为最终结果。

unflatten是graphviz/bin下的工具,作用就是让图更加紧凑。命令的解释:
-c会把孤立结点摆成一列,5为最大列长;
-Gratio设为fill后,对-Gsize的修改才有意义,图像会填充你指定大小的画布;
-T指定输出格式,-o指定输出文件。

结果示意:
在这里插入图片描述
虽然内容还是很多、图片还是很宽,但这是因为分析了5个文件,代码量将近3000行。
这个代码我只用过一次,对graphviz的使用也完全是照搬,欢迎评论指正。

3.附录:dot语言中结点的命名规则

graphviz的结点可以超过26个,只要能正确命名就可以,不一定要用单个的大写字母。
在这里插入图片描述

  • 1
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
可以使用Python的异步库asyncio来实现异步调用多个.py文件代码。具体实现步骤如下: (1)在主函数中创建一个异步事件循环loop; (2)定义异步函数async def func(),在其中使用Python的subprocess库来调用多个.py文件,并开启异步执行; (3)使用Python的asyncio库中的gather()函数来将多个协程封装为一个Task任务,用Task.add_done_callback()函数来指定多任务执行结束后的回调函数; (4)在主函数中通过loop.run_until_complete()函数将异步任务加入到事件循环中,让任务异步执行。 具体代码参考如下: ``` import asyncio import subprocess async def call_pyfile(file_path): process = await asyncio.create_subprocess_exec( 'python', file_path, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE) stdout, stderr = await process.communicate() return (stdout.decode().strip(), stderr.decode().strip()) async def func(): tasks = [call_pyfile('file1.py'), call_pyfile('file2.py'), call_pyfile('file3.py')] results = await asyncio.gather(*tasks) print(results) async def main(): await func() if __name__ == '__main__': loop = asyncio.get_event_loop() loop.run_until_complete(main()) ``` 在上述代码中,我们将三个.py文件的路径传递给了call_pyfile()函数,并在其中使用subprocess库来异步执行这些文件。在func()函数中,我们调用了asyncio库的gather()函数来将三个异步协程封装为一个Task任务,在所有协程执行结束后,将结果打印出来。在主函数中,我们通过loop.run_until_complete()函数将异步任务func()加入到事件循环中,让其异步执行。
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值