发布于

【python】通过 codemap 分析 AnimatedDrawings

code is dumb,codemap is clever

前言

阅读源代码是软件工程师进阶的必备技能,但是现目前通过 IDE 阅读源代码的方式却令人痛苦不已。单行的代码大家都认识,初级的语法大多都不构成真正的障碍,然而阅读一个成熟或者开源项目,却往往令人倍感头疼。

复杂的嵌套关系,冗余的依赖结构,往往使只希望研究项目核心机制的我们,迷失在代码的汪洋之中。

codemap 是一款支持自动跳转,通过连线、高亮、标注等方式辅助阅读源代码的工具,通过 codemap,阅读源代码不再困难。

AnimatedDrawings 是 Facebook 实验室发表的论文《A Method for Animating Children's Drawings of the Human Figure》的附属代码仓库,如果通过 IDE 来分析源代码,会让人倍感痛苦,但通过 codemap 你会发现阅读源代码原来 so easy!

一、安装

官网的 readme 文档中有详实的安装教程,按照指引能在本地快速搭建起项目的运行环境。但是,在 2023.9.1,笔者在 mac m2 环境按照安装教程执行 pip install -e . 命令时,会报如下错误

Collecting PyYAML==6.0 (from animated-drawings==0.0.0)
  Using cached https://pypi.tuna.tsinghua.edu.cn/packages/36/2b/61d51a2c4f25ef062ae3f74576b01638bebad5e045f747ff12643df63844/PyYAML-6.0.tar.gz (124 kB)
  Installing build dependencies ... done
  Getting requirements to build wheel ... error
  error: subprocess-exited-with-error
  
  × Getting requirements to build wheel did not run successfully.
  │ exit code: 1
  ╰─> [48 lines of output]
      running egg_info
      writing lib/PyYAML.egg-info/PKG-INFO
      writing dependency_links to lib/PyYAML.egg-info/dependency_links.txt
      writing top-level names to lib/PyYAML.egg-info/top_level.txt
      Traceback (most recent call last):
        File "/Users/guopengshan/anaconda3/envs/animated_drawiings/lib/python3.8/site-packages/pip/_vendor/pyproject_hooks/_in_process/_in_process.py", line 353, in <module>
          main()
        File "/Users/guopengshan/anaconda3/envs/animated_drawiings/lib/python3.8/site-packages/pip/_vendor/pyproject_hooks/_in_process/_in_process.py", line 335, in main
          json_out['return_val'] = hook(**hook_input['kwargs'])
        File "/Users/guopengshan/anaconda3/envs/animated_drawiings/lib/python3.8/site-packages/pip/_vendor/pyproject_hooks/_in_process/_in_process.py", line 118, in get_requires_for_build_wheel
          return hook(config_settings)
        File "/private/var/folders/q3/_l6msmp54b35hjdmmkq4x_jr0000gn/T/pip-build-env-p82sooh8/overlay/lib/python3.8/site-packages/setuptools/build_meta.py", line 355, in get_requires_for_build_wheel
          return self._get_build_requires(config_settings, requirements=['wheel'])
        File "/private/var/folders/q3/_l6msmp54b35hjdmmkq4x_jr0000gn/T/pip-build-env-p82sooh8/overlay/lib/python3.8/site-packages/setuptools/build_meta.py", line 325, in _get_build_requires
          self.run_setup()
        File "/private/var/folders/q3/_l6msmp54b35hjdmmkq4x_jr0000gn/T/pip-build-env-p82sooh8/overlay/lib/python3.8/site-packages/setuptools/build_meta.py", line 341, in run_setup
          exec(code, locals())
        File "<string>", line 288, in <module>
        File "/private/var/folders/q3/_l6msmp54b35hjdmmkq4x_jr0000gn/T/pip-build-env-p82sooh8/overlay/lib/python3.8/site-packages/setuptools/__init__.py", line 107, in setup
          return distutils.core.setup(**attrs)
        File "/private/var/folders/q3/_l6msmp54b35hjdmmkq4x_jr0000gn/T/pip-build-env-p82sooh8/overlay/lib/python3.8/site-packages/setuptools/_distutils/core.py", line 185, in setup
          return run_commands(dist)
        File "/private/var/folders/q3/_l6msmp54b35hjdmmkq4x_jr0000gn/T/pip-build-env-p82sooh8/overlay/lib/python3.8/site-packages/setuptools/_distutils/core.py", line 201, in run_commands
          dist.run_commands()
        File "/private/var/folders/q3/_l6msmp54b35hjdmmkq4x_jr0000gn/T/pip-build-env-p82sooh8/overlay/lib/python3.8/site-packages/setuptools/_distutils/dist.py", line 969, in run_commands
          self.run_command(cmd)
        File "/private/var/folders/q3/_l6msmp54b35hjdmmkq4x_jr0000gn/T/pip-build-env-p82sooh8/overlay/lib/python3.8/site-packages/setuptools/dist.py", line 1233, in run_command
          super().run_command(command)
        File "/private/var/folders/q3/_l6msmp54b35hjdmmkq4x_jr0000gn/T/pip-build-env-p82sooh8/overlay/lib/python3.8/site-packages/setuptools/_distutils/dist.py", line 988, in run_command
          cmd_obj.run()
        File "/private/var/folders/q3/_l6msmp54b35hjdmmkq4x_jr0000gn/T/pip-build-env-p82sooh8/overlay/lib/python3.8/site-packages/setuptools/command/egg_info.py", line 319, in run
          self.find_sources()
        File "/private/var/folders/q3/_l6msmp54b35hjdmmkq4x_jr0000gn/T/pip-build-env-p82sooh8/overlay/lib/python3.8/site-packages/setuptools/command/egg_info.py", line 327, in find_sources
          mm.run()
        File "/private/var/folders/q3/_l6msmp54b35hjdmmkq4x_jr0000gn/T/pip-build-env-p82sooh8/overlay/lib/python3.8/site-packages/setuptools/command/egg_info.py", line 549, in run
          self.add_defaults()
        File "/private/var/folders/q3/_l6msmp54b35hjdmmkq4x_jr0000gn/T/pip-build-env-p82sooh8/overlay/lib/python3.8/site-packages/setuptools/command/egg_info.py", line 587, in add_defaults
          sdist.add_defaults(self)
        File "/private/var/folders/q3/_l6msmp54b35hjdmmkq4x_jr0000gn/T/pip-build-env-p82sooh8/overlay/lib/python3.8/site-packages/setuptools/command/sdist.py", line 113, in add_defaults
          super().add_defaults()
        File "/private/var/folders/q3/_l6msmp54b35hjdmmkq4x_jr0000gn/T/pip-build-env-p82sooh8/overlay/lib/python3.8/site-packages/setuptools/_distutils/command/sdist.py", line 251, in add_defaults
          self._add_defaults_ext()
        File "/private/var/folders/q3/_l6msmp54b35hjdmmkq4x_jr0000gn/T/pip-build-env-p82sooh8/overlay/lib/python3.8/site-packages/setuptools/_distutils/command/sdist.py", line 336, in _add_defaults_ext
          self.filelist.extend(build_ext.get_source_files())
        File "<string>", line 204, in get_source_files
        File "/private/var/folders/q3/_l6msmp54b35hjdmmkq4x_jr0000gn/T/pip-build-env-p82sooh8/overlay/lib/python3.8/site-packages/setuptools/_distutils/cmd.py", line 107, in __getattr__
          raise AttributeError(attr)
      AttributeError: cython_sources
      [end of output]
  
  note: This error originates from a subprocess, and is likely not a problem with pip.
error: subprocess-exited-with-error

× Getting requirements to build wheel did not run successfully.
│ exit code: 1
╰─> See above for output.

通过 debug,发现是 PyYAML 依赖版本异常导致,只需要将 PyYAML 升级为 6.0.1 即能恢复正常,如下图修改 setup.py 文件即可

二、IDE 实战分析

安装正常后,按照文档说明,在本地运行

from animated_drawings import render
render.start('./examples/config/mvc/interactive_window_example.yaml')

便能启动基础范例。

通常,我们会通过入口文件,在 IDE 中逐个文件或函数查看代码的具体实现机制。

比如 render.start('./examples/config/mvc/interactive_window_example.yaml') 这段代码,首先调用了配置文件 interactive_window_example.yaml,然后通过 render 的 start 方法启动。

接着,我们会发现 interactive_window_example.yaml 又调用了 char_cfg.yaml、dab.yaml、fair1_ppf.yaml 文件,但各个文件的具体功能对于初学者的我们却无从得知。render.start 方法首先初始化了 cfg 实例,然后通过 cfg 中的具体配置,分别创建了 view、scene、controller 等实例,最后通过 controller.run() 启动。

在 controller.run 中首先执行了 self._prep_for_run_loop(),然后通过检测是否 self._is_run_over() 执行死循环,包括 self._start_run_loop_iteration()、self._update()、self._render()、self._tick()、self._handle_user_input()、self._finish_run_loop_iteration() 等更新算法,最后执行 self._cleanup_after_run_loop()。

而针对更具体的 self._prep_for_run_loop()、self._prep_for_run_loop() 等更新算法的实现机制,我们发现他们在 Controller 类中都是通过抽象方法 @abstractmethod 定义的,也就是说对于具体的执行流程,要查看具体执行的函数需要看在函数的运行流程中,具体是哪个 Controller 的子类被实例化了,那个子类实现的 self._prep_for_run_loop()、self._prep_for_run_loop() 等方法才是真实被执行的代码,因此,我们需要跳转到 render.py 文件中,查看 controller 的实例过程。

通过代码,我们发现 controller 的具体实现类是由 cfg.mode 决定,即当 config 中 mode 为 video_render 时,controller 为 VideoRenderController 的实例,当 mode 为 interactive 时,controller 为 InteractiveController 的实例。也就是说,具体执行的 self._prep_for_run_loop()、self._prep_for_run_loop() 等更新算法由 config 中的 mode 决定,当 mode 为 video_render 时,程序流中执行的就是 VideoRenderController 中的 self._prep_for_run_loop()、self._prep_for_run_loop(),否则就是 InteractiveController 中的 self._prep_for_run_loop()、self._prep_for_run_loop()。而具体的 mode 值,在 interactive_window_example.yaml 中,interactive_window_example.yaml 又依赖 char_cfg.yaml、dab.yaml、fair1_ppf.yaml,我们需要不停地在不同的文件夹中打开不同的文件,不停地查找、切换,才能验证这个最简单的范例究竟是怎么执行的。

为了这么简单的需求,我们不停地打开文件,不停地切换,对于初学者,大脑早就一团乱麻,不必要的思维负担已经消耗了绝大多数的脑力,而对于更重要的具体更新机制,却还一筹莫展。

code is dumb,这就是 codemap 需要解决的问题!

三、codemap 实战分析

codemap 摒弃了 tab 页的打开方式,通过平铺布局,以及连线、高亮、标注等一系列手段,辅助用户阅读源代码,告别了为了研究代码的执行机制,而不停切换文件,不断展开、折叠源代码的历史,为用户提供了一种可以清晰展示代码逻辑结构、添加高亮备注的方式。

codemap 目前已经支持 js、ts、c、c++、Java、golang、python 等多种主流编程语言,未来还将支持更多。

单行的代码大家都认识,但是复杂的项目为什么就看不懂了呢?其中的关键在于阅读源代码的过程中,我们的大脑做了太多的无用功,在现有的 ide 中需要记忆大量无关紧要的中间函数、路径,甚至所在代码的行数,而通过 codemap,我们能很好地解决这个问题。

1、回到上述问题,cfg.mode 的具体取值影响了程序流,但在用户显式加载的配置文件 interactive_window_example.yaml 等中并没有 mode 配置项,着眼于程序示例 1(蓝色标记),发现 Config 类会加载默认配置 mvc_base_cfg.yaml,在该文件中有配置项 MODE: 'interactive',因此可以证实 controller 是 InteractiveController 的实例。

2、在程序示例 2(蓝色标记)处,展示了 controller.run()的执行步骤,但是具体的执行函数在 Controller 父类中都是抽象方法,具体的实现在子类 InteractiveController 中。

3、如图示例 3(蓝色标记),InteractiveController 会依次执行:

  1. _prep_for_run_loop:更新 self.prev_time 为当前时间
  2. _is_run_over:通过 glfw.window_should_close(self.view.win)来判断是否需要执行以下循环
  3. _start_run_loop_iteration:调用 self.view.clear_window(),实际是通过 OpenGL 清理窗口
  4. _update:通过平移、旋转、缩放矩阵相乘,求最新的矩阵
  5. _render:首先通过 view._update_shaders_view_transform()等更新 camera 信息,然后调用 scene.draw()渲染
  6. _tick:更新时间
  7. _handle_user_input:处理用户输入
  8. _finish_run_loop_iteration:交换缓存
  9. _cleanup_after_run_loop:执行清理程序

四、code is dumb, codemap is clever

通过 codemap(https://codemap.info)进行分析,我们已经大致地了解了 AnimatedDrawings 的启动流程,而且期间不会再因为不停地打开、切换文件,不停折叠代码、不停跳转而烦恼了,这次进行了分析,下次还能在今天的基础上继续研究。

通常,对于越复杂的项目,codemap 的效果越好,想象一下,你要打开几十个文件,每个文件里面都是你不熟悉的类和函数,对于他们之间的调用、依赖关系,如果纯用脑力,我相信你会爆炸的。

评论(1)