当前位置: 首页 > 科技观察

Python进阶源码解析:如何将一个类的方法变成多个方法?

时间:2023-03-15 13:46:49 科技观察

在之前的文章《Python 中如何实现参数化测试?》中,提到了几个用Python实现参数化测试的库,留下了一个疑问:他们是如何把一个方法做成多个方法的,并且每个方法如何绑定一个方法对应的参数?我们再细化一下。原问题是:在一个类中,如何使用装饰器将一个类方法变成多个类方法(或产生类似的效果)?#一个方法只有一个的测试类classTestClass:deftest_func(self):pass#使用装饰器生成multipleclassmethodsclassTestClass:deftest_func1(self):passdeftest_func2(self):passdeftest_func3(self):passPython中装饰器的本质是模仿,用新的方法替换被装饰的方法。在实现参数化的过程中,我们介绍的这几个库使用了哪些方法/秘密武器?1、ddt是如何实现参数化的?先回顾一下上一篇ddt库的写法:importunittestfromddtimportddt,data,unpack@ddtclassMyTest(unittest.TestCase):@data((3,1),(-1,0),(1.2,1.0))@unpackdeftest(self,first,second):passddt可以提供4个装饰器:1个@ddt添加到类中,3个@data、@unpack和@file_data添加到类方法中(上面没有提到)。先看类方法中添加的三个装饰器的作用:#ddtversion(win):1.2.1defdata(*values):globalindex_lenindex_len=len(str(len(values)))returnidata(values)defidata(iterable):defwrapper(func):setattr(func,DATA_ATTR,iterable)returnfuncreturnwrapperdefunpack(func):setattr(func,UNPACK_ATTR,True)返回funcdeffile_data(value):defwrapper(func):setattr(func,FILE_ATTR,value)return它们共同的returnfunc功能是在类方法上的setattr()中添加属性。至于什么时候用到这些属性?我们看一下类中添加的@ddt装饰器的源码:第一层for循环遍历所有类方法,然后有if/elif的两个分支,分别对应DATA_ATTR/FILE_ATTR,即两个源相应的参数:数据(@data)和文件(@file_data)。elif分支有解析文件的逻辑,然后类似处理数据,所以我们略过,主要看前面的if分支。这部分逻辑很清晰,主要任务如下:遍历类方法的参数键值对根据原方法和参数对创建新的方法名获取原方法的文档字符串解决元组和列表类型的参数包在测试类中添加了新的测试方法,并绑定了参数和文档字符串来分析源码。可以看出@data、@unpack和@file_data这三个装饰器主要是设置属性和传递参数,而@ddt装饰器是核心处理逻辑。这种分散装饰器(将它们分别添加到类和类方法中)并组合它们的方案非常不优雅。为什么它们不能一起使用?后面我们会分析它难言的隐藏含义,我们先按下按钮,看看其他的实现是怎么样的?2、parameterized如何实现参数化?先回顾一下上一篇写的参数化库:importunittestfromparameterizedimportparameterizedclassMyTest(unittest.TestCase):@parameterized.expand([(3,1),(-1,0),(1.5,1.0)])deftest_values(self,first,second):self.assertTrue(first>second)提供了一个装饰器类@parameterized,源码如下(0.7.1版本),主要做一些初始验证和参数分析,不是我们关注的重点,跳过它。我们主要关注这个装饰器类的expand()方法。它的文档评论说:参数化测试用例的“蛮力”方法。创建新的测试用例并将它们注入到包装函数定义所在的命名空间中。对于“UnitTest”子类中的参数化测试很有用,其中Nose测试生成器不起作用。两个关键动作是:“createsnewtestcases(创建新的测试单元)”和“injectthemintothenamespace...(注入到原方法的命名空间)”。关于第一点,它和ddt类似,只是在命名风格、参数解析和绑定等方面有些区别,不值得过多关注。最不同的是如何使新的测试方法有效?parameterized使用了一种“注入”的方法:inspect是一个强大的标准库,在这里用来获取程序调用栈的信息。前三段代码的目的是为了取出f_locals,意思是“本帧看到的局部命名空间”,其中f_locals指的是类的局部命名空间。说到局部命名空间,大家可能会想到locals()。但是,我们在之前的文章中提到过“locals()和globals()之间的读写问题”。locals()是可读但不可写的,所以这段代码只用到了f_locals。3、pytest是如何实现参数化的?按照惯例,先看上一篇的写法:importpytest@pytest.mark.parametrize("first,second",[(3,1),(-1,0),(1.5,1.0)])deftest_values(first,second):assert(first>second)先看到“mark”,pytest中内置了一些标签,比如parametrize,timeout,skipif,xfail,tryfirst,trylast等定义好的标签,可以设置执行conditions,groupandfilterexecution,andmodifytheoriginaltestbehavior等。用法也很简单,但是它的源代码可能要复杂得多。这里我们只关注parametrize,先来看看核心代码:根据传入的参数对,复制原测试方法的调用信息,存入待调用列表中。与上面分析的两个库不同的是,这里并没有创建新的测试方法,而是复用已有的方法。查找parametrize()所属的Metafunc类,可以追踪到_calls列表的使用位置:最终是在Function类中执行的:有趣的是,在这里我们可以看到几行大神的注释……阅读(粗略阅读)pytest的源码,真是自找麻烦。。。但是,依稀可以看出,它在实现参数化的时候,使用了生成器方案,在遍历一个参数的时候,调用了一个测试方法,而前面的ddt和参数化就是一次性解析所有的参数,生成n个新的测试方法,然后交给测试框架调度。相比之下,前两个库的思路非常清晰,并且由于它们的设计纯粹是为了参数化,不像pytest那样有任何标记和太多的抽象设计,因此更容易阅读和理解。前两个库利用了Python的动态特性,设置类属性或注入局部命名空间,而pytest似乎借鉴了一些静态语言的思想,有点笨拙。4、最后总结回到标题“如何将一种方法变成多种方法?”中的问题。除了参数化测试之外,还有哪些其他场景会具有这种吸引力?本文分析了三个测试库Ideas中装饰器的实现,通过阅读源码,我们可以发现它们各有优缺点,这个发现本身就很有趣。使用装饰器时,它们表面上看起来很相似,但真正的细节隐藏在下面。源码分析的意义在于探究为什么。在这次探索之旅中,读者能收获什么?一起聊聊吧!