B.4 使用IPython进行高效代码开发的技巧
对于很多用户来说,以易于开发、调试和最终交互使用的方式编写代码可能是一种习惯的改变。代码重新加载等程序细节可能需要一些调整以及编码风格方面的考虑。
因此,实现本节中所介绍的大多技巧更多的是艺术而不是科学,并且需要你进行一些实验来确定一种对你有效的Python代码编写方法。最终,希望以易于迭代使用的方式构建代码,并能够尽可能轻松地探索程序或函数运行的结果。使用IPython设计的软件比仅用作独立命令行应用程序的代码更易于使用。这一点变得尤为重要,特别是出现了问题使你不得不对程序中你自己或别人在数月或数年前写的代码进行检查的时候。
B.4.1 重载模块依赖项
在Python中,当输入import some_lib
时,some_lib中的代码将被执行,并且所有定义的变量、函数和导入都将存储在新创建的some_lib模块命名空间中。之后再输入import some_lib
时,将获得对现有模块命名空间的引用。在交互式IPython代码开发中可能会遇到潜在的困难,比如当以%run
命令运行一个依赖于其他模块的脚本,而依赖的模块可能已经做了修改的时候。假设在test_script.py中有以下代码:
import some_lib
x = 5
y = [1, 2, 3, 4]
result = some_lib.get_answer(x, y)
如果要执行%run test_script.py
,然后修改some_lib.py,则下次执行%run test_script.py
时,由于Python模块系统是“一次加载”的,仍然会得到旧版本的some_lib.py。这种行为不同于其他数据分析环境,如MATLAB,它会自动传播代码的变更。
由于模块或包可以在特定程序的许多不同位置导入,因此Python在第一次导入模块时会缓存模块的代码,而不是每次都在模块中执行代码。否则,模块化和良好的代码组织可能会导致应用程序效率低下。
为了解决这个问题,有几个选择。第一种方法是在标准库的importlib模块中使用reload
函数:
import some_lib
import importlib
importlib.reload(some_lib)
上面的代码保证每次运行test_script.py时都会得到一个新的some_lib.py副本。显然,如果依赖关系变得更深,那么在整个地方插入reload的用法可能有点棘手。对于这个问题,IPython有一个特殊的dreload
函数(不是一个魔术函数),用于模块的“深层”(递归)重载。如果要运行some_lib.py然后输入dreload(some_lib)
,将尝试重新加载some_lib及其所有依赖项。不幸的是,并不是所有的情况下都有效,有时不得不重新启动IPython。
B.4.2 代码设计技巧
这里并没有什么简单的方法,但是有一些高级的准则。
B.4.2.1 保持相关对象和数据的存在
看到结构有点像下面这个简单的例子的命令行代码并不罕见:
from my_functions import g
def f(x, y):
return g(x + y)
def main():
x = 6
y = 7.5
result = x + y
if __name__ == '__main__':
main()
如果要在IPython中运行该程序,什么地方可能会出错?完成后,在main函数中定义的结果或对象都不会在IPython shell中访问。更好的方法是直接在模块的全局命名空间中,执行main中所有代码(如果想要让模块也变得可导入的话,则在if __name__=='__main__':
代码块中执行)。这样,当运行代码时,就能够看到main中定义的所有变量。这种方式和在Jupyter notebook中在代码单元内定义顶层变量的方式是等价的。
B.4.2.2 扁平优于嵌套
深度嵌套的代码联想到洋葱一层层的皮。在测试或调试某个功能时,为了达到感兴趣的代码,必须剥下多少层洋葱?”扁平优于嵌套“的观念是Python之禅的一部分,开发交互式代码的时候这种观念依然有用。使函数和类尽可能地解耦并模块化,可以使得它们更易于测试(如果正在编写单元测试)、调试以及交互式使用。
B.4.2.3 克服对长文件的恐惧
如果你有Java背景(或者其他什么语言),你可能已经知道要保持文件短小。在很多语言中,这听起来只是个建议,冗长通常是一种不好的"代码味道",这表明重构和重组可能是必要的。但是,在使用IPython开发代码的同时,使用10个小但内部关联的文件(每个文件不超过100行)比两三个更长的文件可能会感到更加头痛。更少的文件意味着更少的模块重新加载,并且在编辑时也减少了文件之间的跳跃。维护更大的模块,使每个模块都具有很高的内部凝聚力,更加有用也更加Pythonic。向解决方案进行迭代之后,有时候将较大的文件重构为较小的文件是有意义的。
显然,不要将这个论点推向极端,这将会把所有代码放在一个单一的怪异文件中。为大型代码库寻找一个合理且直观的模块及包结构通常需要不少工作,但是团队合作尤为重要。每个模块应该在内部具有内聚性,并且在哪里找到负责每个功能区域的功能和类应该尽可能明显。
B.5 高阶IPython特性
充分利用IPython系统可能会用稍微不同的方式编写代码,或者深入了解配置。
B.5.1 使自定义的类对IPython友好
IPython会尽一切努力显示对控制台友好的字符串,这些字符串表示的是想检查的对象。对于许多对象,如字典、列表和元组,内置的pprint
模块可以很好地完成格式化。但是,在用户定义的类中,必须自己生成所需的字符串输出。假设有以下简单的类:
class Message:
def __init__(self, msg):
self.msg = msg
如果写了上面的代码,会失望地发现这个类的默认输出不是很好:
x = Message('I have a secret')
x
<__main__.message at>
IPython获取的输出字符串是由__repr__
的魔术方法返回的(通过语句output=repr(obj)
),并将输出打印到控制台。因此,可以增加一个简单的__repr__
方法到之前的类,就可以获得更有用的输出:
class Message:
def __init__(self, msg):
self.msg = msg
def __repr__(self):
return 'Message: %s' % self.msg
x = Message('I have another secret')
x
Message: I have another secret
B.5.2 配置文件与配置
IPython的大部分外观选项(颜色、提示和线条间距等)以及IPython和Jupyter环境的行为可以通过广泛的设置系统进行配置。下面这些事情都可以通过配置来完成:
更改颜色主题
更改输入输出的外观,或者去除Out之后和下一个In之前的空白行
执行任意的Python语句列表(例如,导入你总是使用的库,或者是其他你希望每次你启动IPython就运行的程序)
始终启用IPython扩展,如line_profiler中的%lprun魔术函数
激活Jupyter拓展
自定义魔术函数或系统别名
IPython shell的配置在专门的ipython_config.py文件中指定,这些文件通常位于用户主目录中的.ipython/目录中。配置是基于特定配置文件执行的。当正常启动IPython时,默认情况下会加载存储在profile_default目录中的默认配置文件。因此,在Linux操作系统上,默认IPython配置文件的完整路径是:
要在自己的系统上初始化该文件,在终端运行下面的指令:
ipython profile create secret_project
这个文件里的内容。幸运的是它有注释,描述每个配置选项的用途。另一个有用的功能是可以有多个配置文件。假设想要为特定应用程序或项目定制另一套IPython配置。创建一个新的配置文件就像输入下面代码一样简单:
完成此操作后,编辑新创建的profle_secret_project目录中的配置文件,然后启动IPython,如下所示:
ipython --profile=secret_project
和通常情况一致,IPython的官方在线文档是对于配置文件和配置是一个非常好的资源。
Jupyter的设置略有不同,因为在Juypter的notebook中使用的不只是Python语言。要生成类似的Jupyter配置文件,运行下面指令:
jupyter notebook --generate-config
上面的代码会将默认配置文件写入主目录下的.jupyter/jupyter_notebook_confg.py
目录。将配置文件编辑到符合需求后,将它重命名为不同的 文件,比如:
mv ~/.jupyter/jupyter_notebook_config.py ~/.jupyter/my_custom_config.py
在启动Jupyter的时候,可以添加--config
参数:
jupyter notebook --config=~/.jupyter/my_custom_config.py
B.6 附录小结
可以在nbviewer网站(https://nbviewer.jupyter.org/ )上发现很多感兴趣的Jupyter notebook。
欢迎关注我的微信公众号一起交流!