当前位置: 首页 > 后端技术 > Python

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

时间:2023-03-25 23:03:22 Python

作者:猫下豌豆花来源:Python猫之前的文章《python 中如何实现参数化测试?》,提到了几个python实现参数化测试的库,留下了一个疑问:它们是如何将一个方法实现成多个方法,并绑定每个方法都有相应的参数?我们再细化一下,原问题等于:在一个类中,如何使用装饰器将一个类方法变成多个类方法(或产生类似的效果)?一个只有一个方法的测试类classTestClass:deftest_func(self):pass使用装饰器生成多个类方法classTestClass:deftest_func1(self):passdeftest_func2(self):passdeftest_func3(self):pass在Python中装饰器物的本质是用一种新的方法代替装饰方法。在实现参数化的过程中,我们介绍的几个库用了哪些方法/秘密武器?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添加到类方法中(上面没有提到)。先看类方法中添加的三个装饰器的作用:ddt版本(win):1.2.1defdata(*values):globalindex_lenindex_len=len(str(len(values)))returnidata(values)defidata(iterable):defwrapper(func):setattr(func,DATA_ATTR,iterable)返回funccreturnwrapperdefunpack(func):setattr(func,UNPACK_ATTR,True),FILE_ATTR,value)returnfuncreturnwrapper他们共同的作用是给类方法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)首先看到“标记”。pytest内置了一些标签,比如parametrize、timeout、skipif、xfail、tryfirst、trylast等。它还支持用户自定义标签,可以设置执行条件,分组和过滤执行,修改原有的测试行为,etc.用法也很简单,但是,它的源代码可能要复杂得多。这里我们只关注parametrize,先来看看核心代码:根据传入的参数对,复制原测试方法的调用信息,存入待调用列表中。与上面分析的两个库不同的是,这里并没有创建新的测试方法,而是复用已有的方法。查找parametrize()所属的Metafunc类,可以追踪到_calls列表的使用位置:最终是在Function类中执行的:有趣的是,在这里我们可以看到几行大神的注释……阅读(粗略阅读)pytest的源码真是自找麻烦。。。不过,依稀可以看出,它在实现参数化的时候,使用的是生成器方案,遍历一个参数调用一个测试方法,而前面的ddt和parameterized就是一次性解析所有的参数,生成n个新的测试方法,然后交给测试框架进行调度。相比之下,前两个库的思路非常清晰,并且由于它们的设计纯粹是为了参数化,不像pytest那样有任何标记和太多的抽象设计,因此更容易阅读和理解。前两个库利用了Python的动态特性,设置类属性或注入局部命名空间,而pytest似乎借鉴了一些静态语言的思想,有点笨拙。4、最后总结回到标题“如何将一种方法变成多种方法?”中的问题。除了参数化测试,还有哪些场景会具有这种吸引力?欢迎留言讨论。本文分析了三个测试库的装饰器实现思路。通过阅读源码,我们可以发现它们各有优缺点。这个发现本身就很有趣。使用装饰器时,它们表面上看起来很相似,但真正的细节隐藏在下面。源码分析的意义在于探究为什么。读者能从这次探索之旅中收获什么?一起聊聊吧!