Python探索之Metaclass初步了解
先以一个大牛的一段关于PythonMetapgramming的著名的话来做开头:
Metaclassesaredeepermagicthan99%ofusersshouldeverworryabout.Ifyouwonderwhetheryouneedthem,youdon't(thepeoplewhoactuallyneedthemknowwithcertaintythattheyneedthem,anddon'tneedanexplanationaboutwhy).–TimPeters
翻译一下:Metaclasses是99%的用户都无需费神的黑科技。如果你还在纠结你是不是需要它的话,答案是NO(真正需要的人根本不需要解释)–TimPeters
这是什么鬼话?道可道,非常道吗?
Meta?
好,装B已毕。这确实是一个冷僻的,不常用的话题。一篇短文肯定讲不完。所以叫做初步了解。
python中的类
首先这里讨论的python类,都基于继承于object的新式类进行讨论。
首先在python中,所有东西都是对象。这句话非常重要要理解元类我要重新来理解一下python中的类
classTrick(object): pass
当python在执行带class语句的时候,会初始化一个类对象放在内存里面。例如这里会初始化一个Trick对象
这个对象(类)自身拥有创建对象(通常我们说的实例,但是在python中还是对象)的能力。
为了方便后续理解,我们可以先尝试一下在新式类中最古老厉害的关键字type。
input: classTrick(object): pass printtype('123') printtype(123) printtype(Trick()) output:
可以看到能得到我们平时使用的str,int,以及我们初始化的一个实例对象Trick()
但是下面的方法你可能没有见过,type同样可以用来动态创建一个类
type(类名,父类的元组(针对继承的情况,可以为空),包含属性的字典(名称和值))
英文meta这个词其实是从希腊语里面借来的。wikipedia上的解释是:
indicateaconceptwhichisanabstractionbehindanotherconcept,usedtocompleteoraddtothelatter
不看还好,其实看了更晕。好在后面的解释有一句“更高一层的抽象”,可以帮助理解。其实我们可以这样理解。meta的意思就是“关于什么的什么”:比如metadata可以理解为“关于数据的数据”,metaprogramming可以理解为“关于编程的编程”。这就和“更高一层的抽象”比较契合了。同时又隐隐和编程中的另一个永恒主题-recursion联系在了一起。
另外,meta这个词天朝这边翻译成“元”,海峡对岸翻译成“后设”。其实我都不大理解从何而来。
元类一般用于创建类。在执行类定义时,解释器必须要知道这个类的正确的元类。解释器会先寻找类属性__metaclass__,如果此属性存在,就将这个属性赋值给此类作为它的元类。如果此属性没有定义,它会向上查找父类中的__metaclass__.如果还没有发现__metaclass__属性,解释器会检查名字为__metaclass__的全局变量,如果它存在,就使用它作为元类。否则,这个类就是一个传统类,并用types.ClassType作为此类的元类。
在执行类定义的时候,将检查此类正确的(一般是默认的)元类,元类(通常)传递三个参数(到构造器):类名,从基类继承数据的元组,和(类的)属性字典。
实例
聚焦到我们今天的主题,metaprogramming就是编写用来生成代码的代码。
假设我们写了一个NB的函数,用来计算一个任意复杂的算数表达式的值:
像1+2,3*6+10,什么的都可以交给它去计算。这样的函数的算法不是我们的主题,所以我们请出python自带的大招eval(),一行就可以搞定了:
defcalc(expression): returneval(expression)
因为输入的可能性是无限的,所以我们肯定要好好测试一下这个函数了。假定我们想了上百个testcase。又假定我们是用unittest这个module来做测试的。这样的测试程序一般会长成这样:
importunittest classTestStringMethods(unittest.TestCase): deftest_upper(self): self.assertEqual('foo'.upper(),'FOO') deftest_isupper(self): self.assertTrue('FOO'.isupper()) self.assertFalse('Foo'.isupper()) deftest_split(self): s='helloworld' self.assertEqual(s.split(),['hello','world']) #checkthats.splitfailswhentheseparatorisnotastring withself.assertRaises(TypeError): s.split(2) if__name__=='__main__': unittest.main()
所以我们的目的就是用metaprogramming的方式来自动产生类似上面的测试类。
先上程序后解释:
#!/usr/bin/python3 importunittest defcalc(expression): returneval(expression) defadd_test(name,asserts): deftest_method(asserts): deffn(self): left,right=asserts.split('=') expected=str(calc(left)) self.assertEqual(expected,right) returnfn d={'test1':test_method(asserts)} cls=type(name,(unittest.TestCase,),d) globals()[name]=cls if__name__=='__main__': fori,tinenumerate([ "1+2=3", "3*5*6=90"]): add_test("Test%d"%i,t) unittest.main()
NB的calc()函数我们解释过了。main这段也比较简单:我们用声明的方式定义了一组测试,然后通过unittest来执行。
有点复杂的是add_test()。我们先来看看最内层的fn(self)这个方法。逻辑上,它就是把输入的测试用例分成两份,一份是calc()的输入,一份是我们期待的结果;然后调用calc(),接着用assertEqual()来测试。
但是这个self有点奇怪–这里没有类,哪里来的self?其实fn(self)确实是一个类的方法,只不过这个类是我们通过代码动态生成的。也就是下面这一行:
cls=type(name,(unittest.TestCase,),d)
这里的type()就是通常我们用来检查某个变量的类型的那个函数。只不过它还有另外一种不大为人知的形式:
classtype(name,bases,dict)
这第二种形式,就会产生一个新的类型。以我们的程序为例,就是以unit.TestCase为baseclass,产生了一个名为TestN的新类型,改类型的实现由d给出,而d就包含了通过closure返回的fn(self)这个方法。只不过在这个新类里面,它的名字叫做test1()。
最后,我们把这个新产生的类加入到当前全局符号表里面,也就相当于上面给出的unittest的例子。
所以,总结一下。当我们运行这个脚本的时候,这段比较短的代码会针对每一个测试的表达式产生一个新的测试类,并动态生成测试的方法加载到该类里面。unitest从globals中找到这些类并一一执行测试。
上面的例子中,其实一行一行手打calc(1+2)==3也没什么大不了的。但是当你要表达的逻辑比较复杂的时候,metaprogramming的强大就体现出来了。
那么,看完这篇文章,我们也成为Tim所说的1%的程序猿了!其实,也许他的意思是,99%的编程工作都用不到这样技巧。在一些特殊的场合,比如编写某种框架的时候,metaprogramming会做到事半功倍。祝你在实践中碰到这样的机会。
以上就是本文关于Python探索之Metaclass初步了解的全部内容,希望对大家有所帮助。感兴趣的朋友可以继续参阅本站:Python编程之Re模块下的函数介绍、python中模块的__all__属性详解等,如有不足之处,欢迎留言指出。感谢朋友们对本站的支持!