Python用得到的黑魔法当然是各种自省和动态绑定了。
举个例子,Python可以重新绑定解释器的excepthook,这样当程序异常结束时就可以做一些自定义的处理,我自己就一直拿这个配合ipdb进行debug。用以下代码声明一个ExceptionHook:
class ExceptionHook :
instance = None
def __call__(self, *args, **kwargs) :
if self.instance is None:
from IPython.core import ultratb
self.instance = ultratb.FormattedTB(mode = "Plain", color_scheme = "Linux", call_pdb = 1)
return self.instance(*args, **kwargs)
然后
import sys
sys.exceptionhook = ExceptionHook()
重设完exceptionhook后,一旦你的代码抛出异常,整个解释器的环境都会被ipdb接管,然后就可以像交互模式下那样使用了。通常我会在里面查一下栈,把必要的对象pickle一下,这样以后复现错误也比较容易。
由于IPython是非GUI的程序,所以即便在SSH里也可以使用这招,完美解决SSH缺少IDE难以debug的窘境。
动态绑定的另一个用处,就是当程序依赖一个修改过的库时,可以把修改的部分剥离出来,在运行时动态绑定到对应的库上去就行。如果修改的是成员方法,需要这样绑定:
from types import MethodType
def _foo(self, ...):
pass
obj.foo = MethodType(_foo, obj)
顺带提一下,pickle也是个非常好用的工具,尽管序列化并不是python的专利。pickle可以用来保存各种运行过程中的对象:
import pickle
pickle.dump(xxx, open("xxx.dump", "w"))
yyy = pickle.load(open("yyy.dump"))
pickle可以减少很多工作量,尤其是在复现bug时,把正确部分的运行结果pickle下来,这样每次可以从pickle的位置开始运行。跑多个相似的baseline时也有很好的效果。不足的是pickle比较吃硬盘,pickle一堆东西后很容易就十几个G了,而且pickle不能序列化动态生成的对象,比如lambda表达式或者上面提到的动态绑定产生的成员方法。
自省方面,Python可以通过dir()和help()函数分别取得对象下成员的列表和帮助,这个在找不到库文档的时候非常好用。只要开发者在函数下面写了注释,就能在help中看到。
除了上面提到的这些特性,python还有一堆小trick,其他回答里也提到了一些。虽然其中很多是语法糖,不过用好它们可以让程序更pythonic:
1 类中用__slots__将成员静态化,可以节省大量内存。
2 装饰器,常见用途如函数计时,亦可用来产生新的函数签名。函数签名会影响传参检查和ide补全,对带不定长参数的函数非常有用。很多库中都会用这种方法来兼容不同版本的API。
3 生成器,对于只需遍历的数据可以节省大量内存。
4 *和**参数展开。典型的例子是zip(*list_x)和chain(*list_x),分别相当于转置和concatenate。
5 if __name__ == "__main__": 检查是否作为主程序调用,用multiprocessing并行时主程序得用这个框起来。
6 enumerate,例如将一个list变成list2index可以用dict([(x, i) for i, x in enumerate(list_x)])
7 namedtuple,生成类似于C语言的结构体,同时支持tuple的所有语法。
8 defaultdict,做统计时不用初始化的dict,可以用lambda实现嵌套构造defaultdict(lambda : defaultdict(int)),甚至递归字典tree = lambda : defaultdict(tree)。