本文档记录了实现切面当中很长时间发现的一个潜在bug,其实和代码本身无关,而是调试过程中IDE有关。如果没有切面和python
装饰器的基础,可以直接看最后一个章节当中bug的复现。
基础功能实现
首先,由于业务需求,我们封装了一个自己的数据结构,为了方便,就以spark
当中的DataFrame
封装为例。首先,我们读取一个简单的表格:
1 | from pyspark.sql import SparkSession |
假设这个数据表长这样:
1 | +---+---+ |
首先,由于业务需求,我们需要封装一个自己的数据结构,就叫他 MyDataFrame
吧,大致框架如下:
1 | from pyspark.sql import DataFrame as SparkDataFrame |
在上面的代码中,__getattr__
和__getitem__
是因为用户可能会通过dataframe获取某一列,即为了满足df.a
和df['a']
这两种用法。因此,到目前为止,就支持以下用法了:
1 | if __name__ == '__main__': |
运行一下,也没有问题,结果为:
1 | +---+---+ |
日志功能添加
在实现了这个功能之后,产品突然提需求了。说希望给自己的每个 API 都加上日志,查看运行的时间以方便后续进行性能优化。毕竟咱们写代码的是面向产品进行服务,那他说的也合理,就得开干了。但是作为写代码的,我们也得好好思考自己怎么实现则个需求,核心目标就以下几点:
- 实现产品要求的功能
- 效率要高,要是成千行代码要加这个功能,这得加到哪天,以及哪天加了其他的api,难以保持log功能一致
- 还是易维护,哪天产品不要这个功能了,得好剥离掉
于是就想到了通过装饰器来实现这个功能。首先,需要实现这样一个装饰器:
1 | import time |
这样的话,核心的MyDataFrame
这么写就可以了,精准且优雅,😏~
1 | class MyDataFrame: |
然后我们重新运行一下原有的代码,结果是这么个样子:
1 | in log: op = __getattr__ (<__main__.MyDataFrame object at 0x7fbfaed66320>, 'a') |
Bug 产生了
在log(func)
的代码中,有的时候并不只是记录时间,还会去干别的事情,这个时候还可能会去对代码进行调试。为了让代码更健壮,我们对可能得异常进行捕获,那现在代码就变成了这个样子了:
1 | def log(func): |
这个时候,代码是可以正常运行的,但是,如果开始调试,就会出现bug。假如我在a=1
那里添加了一个断点,然后开始运行:
在监控窗口中,如果把args
展开,就会报错,报错的信息截图如下:
这个错误只会在展开args
的瞬间出发,并且只在调试过程中产生。甚是神奇。
Bug分析与防范
在上面的演示过程中,实际上,目前我碰到了两类可导致这个问题的错误:
MyDataFrame.shape
len(MyDataFrame)
目前个人的猜测为:在调试的过程中,调试的窗口会对监控的对象执行 len(Object) 和 Object.shape 去获取其信息,这个时候就会触发代码里面的对应操作。而我们在代码中,没有去实现 __getattr__(shape)
这个操作,这最终导致了 bug 的产生。对于len(Obj)
也同理。
Bug 复现
这个问题的本质其实和装饰器没关系,我复现了一下,只需要一下基本的代码,就可以在调试过程中产生意外的信息:
1 | class MyDataFrame: |
在col1=mdf.a
这一行代码添加断点,即可在 console 看到系统调用了mdf.shape
了。如下图所示:
解决方法
因此,要想此类问题不产生,我们需要对获取shape和len这两个信息进行额外的处理。在装饰器中可添加以下代码:
1 | if op == '__getattr__' and args[1] == 'shape': |
在自己实现的类中,建议添加__len__()
方法。
1 | class MyDataFrame: |
当然,最好对.shape
操作也单独处理一下。不过这当中会引发一个新的问题,如果dataframe当中,有一列的列名就叫shape,那就导致歧义了。