B.4 使用IPython高效开发的技巧

方便快捷地写代码、调试和使用是每个人的目标。除了代码风格,流程细节(比如代码重载)也需要一些调整。

因此,这一节的内容更像是门艺术而不是科学,还需要你不断的试验,以达成高效。最终,你要能结构优化代码,并且能省时省力地检查程序或函数的结果。我发现用IPython设计的软件比起命令行,要更适合工作。尤其是当发生错误时,你需要检查自己或别人写的数月或数年前写的代码的错误。

重载模块依赖

在Python中,当你输入import some_lib,some_lib中的代码就会被执行,所有的变量、函数和定义的引入,就会被存入到新创建的some_lib模块命名空间。当下一次输入some_lib,就会得到一个已存在的模块命名空间的引用。潜在的问题是当你%run一个脚本,它依赖于另一个模块,而这个模块做过修改,就会产生问题。假设我在test_script.py中有如下代码:

  1. import some_lib
  2. x = 5
  3. y = [1, 2, 3, 4]
  4. result = some_lib.get_answer(x, y)

如果你运行过了%run test_script.py,然后修改了some_lib.py,下一次再执行%run test_script.py,还会得到旧版本的some_lib.py,这是因为Python模块系统的“一次加载”机制。这一点区分了Python和其它数据分析环境,比如MATLAB,它会自动传播代码修改。解决这个问题,有多种方法。第一种是在标准库importlib模块中使用reload函数:

  1. import some_lib
  2. import importlib
  3. importlib.reload(some_lib)

这可以保证每次运行test_script.py时可以加载最新的some_lib.py。很明显,如果依赖更深,在各处都使用reload是非常麻烦的。对于这个问题,IPython有一个特殊的dreload函数(它不是魔术函数)重载深层的模块。如果我运行过some_lib.py,然后输入dreload(some_lib),就会尝试重载some_lib和它的依赖。不过,这个方法不适用于所有场景,但比重启IPython强多了。

代码设计技巧

对于这单,没有简单的对策,但是有一些原则,是我在工作中发现很好用的。

保持相关对象和数据活跃

为命令行写一个下面示例中的程序是很少见的:

  1. from my_functions import g
  2. def f(x, y):
  3. return g(x + y)
  4. def main():
  5. x = 6
  6. y = 7.5
  7. result = x + y
  8. if __name__ == '__main__':
  9. main()

在IPython中运行这个程序会发生问题,你发现是什么了吗?运行之后,任何定义在main函数中的结果和对象都不能在IPython中被访问到。更好的方法是将main中的代码直接在模块的命名空间中执行(或者在__name__ == '__main__':中,如果你想让这个模块可以被引用)。这样,当你%rundiamante,就可以查看所有定义在main中的变量。这等价于在Jupyter notebook的代码格中定义一个顶级变量。

扁平优于嵌套

深层嵌套的代码总让我联想到洋葱皮。当测试或调试一个函数时,你需要剥多少层洋葱皮才能到达目标代码呢?“扁平优于嵌套”是Python之禅的一部分,它也适用于交互式代码开发。尽量将函数和类去耦合和模块化,有利于测试(如果你是在写单元测试)、调试和交互式使用。

克服对大文件的恐惧

如果你之前是写JAVA(或者其它类似的语言),你可能被告知要让文件简短。在多数语言中,这都是合理的建议:太长会让人感觉是坏代码,意味着重构和重组是必要的。但是,在用IPython开发时,运行10个相关联的小文件(小于100行),比起两个或三个长文件,会让你更头疼。更少的文件意味着重载更少的模块和更少的编辑时在文件中跳转。我发现维护大模块,每个模块都是紧密组织的,会更实用和Pythonic。经过方案迭代,有时会将大文件分解成小文件。

我不建议极端化这条建议,那样会形成一个单独的超大文件。找到一个合理和直观的大型代码模块库和封装结构往往需要一点工作,但这在团队工作中非常重要。每个模块都应该结构紧密,并且应该能直观地找到负责每个功能领域功能和类。