Python中的元类编程入门指引
回顾面向对象编程
让我们先用30秒钟来回顾一下OOP到底是什么。在面向对象编程语言中,可以定义类,它们的用途是将相关的数据和行为捆绑在一起。这些类可以继承其父类的部分或全部性质,但也可以定义自己的属性(数据)或方法(行为)。在定义类的过程结束时,类通常充当用来创建实例(有时也简单地称为对象)的模板。同一个类的不同实例通常有不同的数据,但“外表”都是一样—例如,Employee对象bob和jane都有.salary和.room_number,但两者的房间和薪水都各不相同。
一些OOP语言(包括Python)允许对象是自省的(也称为反射)。即,自省对象能够描述自己:实例属于哪个类?类有哪些祖先?对象可以用哪些方法和属性?自省让处理对象的函数或方法根据传递给函数或方法的对象类型来做决定。即使没有自省,函数也常常根据实例数据进行划分,例如,到jane.room_number的路线不同于到bob.room_number的路线,因为它俩在不同的房间。利用自省,还可以在安全地计算jane所获奖金的同时,跳过对bob的计算,例如,因为jane有.profit_share属性,或者因为bob是子类Hourly(Employee)的实例。
元类编程(metaprogramming)的回答
以上概述的基本OOP系统功能相当强大。但在上述描述中有一个要素没有受到重视:在Python(以及其它语言)中,类本身就是可以被传递和自省的对象。正如前面所讲到的,既然可以用类作为模板来生成对象,那么用什么作为模板来生成类呢?答案当然是元类(metaclass)。
Python一直都有元类。但元类中所涉及的方法在Python2.2中才得以更好地公开在人们面前。PythonV2.2明确地不再只使用一个特殊的(通常是隐藏的)元类来创建每个类对象。现在程序员可以创建原始元类type的子类,甚至可以用各种元类动态地生成类。当然,仅仅因为可以在Python2.2中操作元类,这并不能说明您可能想这样做的原因。
而且,不需要使用定制元类来操作类的生成。一种不太费脑筋的概念是类工厂:一种普通的函数,它可以返回在函数体内动态创建的类。用传统的Python语法,您可以编写:
清单1.老式的Python1.5.2类工厂
Python1.5.2(#0,Jun271999,11:23:01)[...] Copyright1991-1995StichtingMathematischCentrum,Amsterdam >>>defclass_with_method(func): ...classklass:pass ...setattr(klass,func.__name__,func) ...returnklass ... >>>defsay_foo(self):print'foo' ... >>>Foo=class_with_method(say_foo) >>>foo=Foo() >>>foo.say_foo() foo
工厂函数class_with_method()动态地创建一个类,并返回该类,这个类包含传递给该工厂的方法/函数。在返回该类之前,在函数体内操作类自身。new模块提供了更简洁的编码方式,但其中的选项与类工厂体内定制代码的选项不同,例如:
清单2.new模块中的类工厂
>>>fromnewimportclassobj >>>Foo2=classobj('Foo2',(Foo,),{'bar':lambdaself:'bar'}) >>>Foo2().bar() 'bar' >>>Foo2().say_foo() foo
在所有这些情形中,没有将类(Foo和Foo2)的行为直接编写为代码,而是用动态参数在运行时调用函数来创建类的行为。这里要强调的一点是,不仅实例可以动态地创建,而且类本身也可以动态地创建。
元类:寻求问题的解决方案?
元类的魔力是如此之大,以至于99%的用户曾有过的顾虑都是不必要的。如果您想知道是否需要它们,则可以不用它们(那些实际需要元类的人们确实清楚自己需要它们,不需要解释原因)。—Python专家TimPeters
(类的)方法象普通函数一样可以返回对象。所以从这个意义上讲,类工厂可以是类,就象它们可以是函数一样容易,这是显然的。尤其是Python2.2+提供了一个称为type的特殊类,它正是这样的类工厂。当然,读者会认识到type()不象Python老版本的内置函数那样“野心勃勃”—幸运的是,老版本的type()函数的行为是由type类维护的(换句话说,type(obj)返回对象obj的类型/类)。作为类工厂的新type类,其工作方式与函数new.classobj一直所具有的方式相同:
清单3.作为类工厂元类的type
>>>X=type('X',(),{'foo':lambdaself:'foo'}) >>>X,X().foo() (<class'__main__.X'>,'foo')
但是因为type现在是(元)类,所以可以自由用它来创建子类:
清单4.作为类工厂的type后代
>>>classChattyType(type): ...def__new__(cls,name,bases,dct): ...print"Allocatingmemoryforclass",name ...returntype.__new__(cls,name,bases,dct) ...def__init__(cls,name,bases,dct): ...print"Init'ing(configuring)class",name ...super(ChattyType,cls).__init__(name,bases,dct) ... >>>X=ChattyType('X',(),{'foo':lambdaself:'foo'}) AllocatingmemoryforclassX Init'ing(configuring)classX >>>X,X().foo() (<class'__main__.X'>,'foo')
富有“魔力”的.__new__()和.__init__()方法很特殊,但在概念上,对于任何其它类,它们的工作方式都是一样的。.__init__()方法使您能配置所创建的对象;.__new__()方法使您能定制它的分配。当然,后者没有被广泛地使用,但对于每个Python2.2new样式的类(通常通过继承而不是覆盖),都存在该方法。
需要注意type后代的一个特性;它常使第一次使用元类的人们“上圈套”。按照惯例,这些方法的第一个参数名为cls,而不是self,因为这些方法是在已生成的类上进行操作的,而不是在元类上。事实上,关于这点没什么特别的;所有方法附加在它们的实例上,而且元类的实例是类。非特殊的名称使这更明显:
清单5.将类方法附加在所生成的类上
>>>classPrintable(type): ...defwhoami(cls):print"Iama",cls.__name__ ... >>>Foo=Printable('Foo',(),{}) >>>Foo.whoami() IamaFoo >>>Printable.whoami() Traceback(mostrecentcalllast): TypeError:unboundmethodwhoami()[...]
所有这些令人惊讶但又常见的做法以及便于掌握的语法使得元类的使用更容易,但也让新用户感到迷惑。对于其它语法有几个元素。但这些新变体的解析顺序需要点技巧。类可以从其祖先那继承元类—请注意,这与将元类作为祖先不一样(这是另一处常让人迷惑的地方)。对于老式类,定义一个全局_metaclass_变量可以强制使用定制元类。但大多数时间,最安全的方法是,在希望通过定制元类来创建类时,设置该类的_metaclass_类属性。必须在类定义本身中设置变量,因为如果稍后(在已经创建类对象之后)设置属性,则不会使用元类。例如:
清单6.用类属性设置元类
>>>classBar: ...__metaclass__=Printable ...deffoomethod(self):print'foo' ... >>>Bar.whoami() IamaBar >>>Bar().foomethod() foo
用这种“魔力”来解决问题
至此,我们已经了解了一些有关元类的基本知识。但要使用元类,则比较复杂。使用元类的困难之处在于,通常在OOP设计中,类其实做得不多。对于封装和打包数据和方法,类的继承结构很有用,但在具体情形中,人们通常使用实例。
我们认为元类在两大类编程任务中确实有用。
第一类(可能是更常见的一类)是在设计时不能确切地知道类需要做什么。显然,您对它有所了解,但某个特殊的细节可能取决于稍后才能得到的信息。“稍后”本身有两类:(a)当应用程序使用库模块时;(b)在运行时,当某种情形存在时。这类很接近于通常所说的“面向方面的编程(Aspect-OrientedProgramming,AOP)”。我们将展示一个我们认为非常别致的示例:
清单7.运行时的元类配置
%catdump.py #!/usr/bin/python importsys iflen(sys.argv)>2: module,metaklass=sys.argv[1:3] m=__import__(module,globals(),locals(),[metaklass]) __metaclass__=getattr(m,metaklass) classData: def__init__(self): self.num=38 self.lst=['a','b','c'] self.str='spam' dumps=lambdaself:`self` __str__=lambdaself:self.dumps() data=Data() printdata %dump.py <__main__.Datainstanceat1686a0>
正如您所期望的,该应用程序打印出data对象相当常规的描述(常规的实例对象)。但如果将运行时参数传递给应用程序,则可以得到相当不同的结果:
清单8.添加外部序列化元类
%dump.pygnosis.magicMetaXMLPickler <?xmlversion="1.0"?> <!DOCTYPEPyObjectSYSTEM"PyObjects.dtd"> <PyObjectmodule="__main__"class="Data"id="720748"> <attrname="lst"type="list"id="980012"> <itemtype="string"value="a"/> <itemtype="string"value="b"/> <itemtype="string"value="c"/> </attr> <attrname="num"type="numeric"value="38"/> <attrname="str"type="string"value="spam"/> </PyObject>
这个特殊的示例使用gnosis.xml.pickle的序列化样式,但最新的gnosis.magic包还包含元类序列化器MetaYamlDump、MetaPyPickler和MetaPrettyPrint。而且,dump.py“应用程序”的用户可以从任何定义了任何期望的MetaPickler的Python包中利用该“MetaPickler”。出于此目的而编写合适的元类如下所示:
清单9.用元类添加属性
classMetaPickler(type): "Metaclassforgnosis.xml.pickleserialization" def__init__(cls,name,bases,dict): fromgnosis.xml.pickleimportdumps super(MetaPickler,cls).__init__(name,bases,dict) setattr(cls,'dumps',dumps)
这种安排的过人之处在于应用程序程序员不需要了解要使用哪种序列化—甚至不需要了解是否在命令行添加序列化或其它一些跨各部分的能力。
也许元类最常见的用法与MetaPickler类似:添加、删除、重命名或替换所产生类中定义的方法。在我们的示例中,在创建类Data(以及由此再创建随后的每个实例)时,“本机”Data.dump()方法被应用程序之外的某个方法所替代。
使用这种“魔力”来解决问题的其它方法
存在着这样的编程环境:类往往比实例更重要。例如,说明性迷你语言(declarativemini-languages)是Python库,在类声明中直接表示了它的程序逻辑。David在其文章“Createdeclarativemini-languages”中研究了此问题。在这种情形下,使用元类来影响类创建过程是相当有用的。
一种基于类的声明性框架是gnosis.xml.validity。在此框架下,可以声明许多“有效性类”,这些类表示了一组有关有效XML文档的约束。这些声明非常接近于DTD中所包含的那些声明。例如,可以用以下代码来配置一篇“dissertation”文档:
清单10.simple_diss.pygnosis.xml.validity规则
fromgnosis.xml.validityimport* classfigure(EMPTY):pass class_mixedpara(Or):_disjoins=(PCDATA,figure) classparagraph(Some):_type=_mixedpara classtitle(PCDATA):pass class_paras(Some):_type=paragraph classchapter(Seq):_order=(title,_paras) classdissertation(Some):_type=chapter
如果在没有正确组件子元素的情形下尝试实例化dissertation类,则会产生一个描述性异常;对于每个子元素,亦是如此。当只有一种明确的方式可以将参数“提升”为正确的类型时,会从较简单的参数来生成正确的子元素。
即使有效性类常常(非正式)基于预先存在的DTD,这些类的实例也还是将自己打印成简单的XML文档片段,例如:
清单11.基本的有效性类文档的创建
>>>fromsimple_dissimport* >>>ch=LiftSeq(chapter,('ItStarts','Whenitbegan')) >>>printch <chapter><title>ItStarts</title> <paragraph>Whenitbegan</paragraph> </chapter>
通过使用元类来创建有效性类,我们可以从类声明中生成DTD(我们在这样做的同时,可以向这些有效性类额外添加一个方法):
清单12.在模块导入期间利用元类
>>>fromgnosis.magicimportDTDGenerator,\ ...import_with_metaclass,\ ...from_import >>>d=import_with_metaclass('simple_diss',DTDGenerator) >>>from_import(d,'**') >>>ch=LiftSeq(chapter,('ItStarts','Whenitbegan')) >>>printch.with_internal_subset() <?xmlversion='1.0'?> <!DOCTYPEchapter[ <!ELEMENTfigureEMPTY> <!ELEMENTdissertation(chapter)+> <!ELEMENTchapter(title,paragraph+)> <!ELEMENTtitle(#PCDATA)> <!ELEMENTparagraph((#PCDATA|figure))+> ]> <chapter><title>ItStarts</title> <paragraph>Whenitbegan</paragraph> </chapter>
包gnosis.xml.validity不知道DTD和内部子集。那些概念和能力完全由元类DTDGenerator引入进来,对gnosis.xml.validity或simple_diss.py不做任何更改。DTDGenerator不将自身的.__str__()方法替换进它产生的类—您仍然可以打印简单的XML片段—但元类可以方便地修改这种富有“魔力”的方法。
元带来的便利
为了使用元类以及一些可以在面向方面的编程中所使用的样本元类,包gnosis.magic包含几个实用程序。其中最重要的实用程序是import_with_metaclass()。在上例中所用到的这个函数使您能导入第三方的模块,但您要用定制元类而不是用type来创建所有模块类。无论您想对第三方模块赋予什么样的新能力,您都可以在创建的元类内定义该能力(或者从其它地方一起获得)。gnosis.magic包含一些可插入的序列化元类;其它一些包可能包含跟踪能力、对象持久性、异常日志记录或其它能力。
import_with_metclass()函数展示了元类编程的几个性质:
清单13.[gnosis.magic]的import_with_metaclass()
defimport_with_metaclass(modname,metaklass): "Moduleimportersubstitutingcustommetaclass" classMeta(object):__metaclass__=metaklass dct={'__module__':modname} mod=__import__(modname) forkey,valinmod.__dict__.items(): ifinspect.isclass(val): setattr(mod,key,type(key,(val,Meta),dct)) returnmod
在这个函数中值得注意的样式是,用指定的元类生成普通的类Meta。但是,一旦将Meta作为祖先添加之后,也用定制元类来生成它的后代。原则上,象Meta这样的类既可以带有元类生成器(metaclassproducer)也可以带有一组可继承的方法—Meta类的这两个方面是无关的。