Python 中的元类编程

本文探讨了Python中元类的使用误区,并提出了一种利用装饰器进行类初始化的新方法,旨在减少代码复杂性和提高可维护性。
http://hi.baidu.com/minyuanyang/blog/item/cd053a73d3e09b198601b046.html
 Python 中的元类编程
2008-04-24 14:53

Python 中的元类编程,第 3 部分

David Mertz (mertz@gnosis.cx), 开发人员, Gnosis Software, Inc.

2007 年 11 月 22 日


编程时太多的聪明反而会使设计更复杂、代码更脆弱、学习曲线更陡峭,最糟的是,调试也更加困难。Michele 和 David 觉得,这在一定程度上归因于对他们 早期的 Python 元类文章 的接受而引起的聪明过度。在本文中,他们试图帮助程序员避免小聪明,以修正这些错误。

简介

去 年,我参加了 EuroPython 2006 会议。这个会议非常好,组织得很完美,谈话都具有很高的水平,人们也都特别友好。然而,我在这篇文章归属的 Python 社区中注意到了一种令人烦恼的趋势。几乎同时,我的合著者 David Mertz 也在思考一个类似的关于一些提交给 Gnosis Utilities 的补丁程序的问题。这种有争议的趋势就是趋向于耍小聪明。不幸的是,Python 社区的这种聪明以前只局限于 Zope 和 Twisted,现在已变得无处不在。

我们在试验项目和学习过程中并不反对这种聪明。我们的烦恼是,在产品框架上必须符合用户的要求。在本文中,我们希望为避免这种聪明做出小小的贡献,至少在我们比较精通的领域避免元类滥用。

对于本文,我们坚持严肃的立场:我们把在不用元类也能解决问题的情况下使用元类都视为元类滥用。当然,作者的过错也很明显:我们的 关于 Python 中的元类的前几部分 助长了这种做法的流行。Nostra culpa

使用元编程最普通的情况就是创建具有动态生成的属性和方法的类。跟流行的观点相反,这是一个在大多数时候都不需要 而且不想要 自定义元类的工作。

本文适用于两类读者:普通程序员和聪明的程序员。前者知道一些元编程技巧,但是并没有在大脑中形成具体的概念;后者很聪明,而且理解得深一些。后者 的问题 在于变得聪明很容易,要变得不那么聪明就得花不少时间了。例如,花几个月时间就能理解如何使用元类,但是要花几年时间才能明白如何 使用它们。




回页首


关于类初始化

创建类的过程中,类的所有属性和方法只设置一次。而在 Python 中,方法和属性随时都可以更改,但是只有不遵守规则的程序员才会这样做。

在各种条件下,创建类时,也许想用比简单地运行静态编码更加动态的方法。例如,可能想根据从配置文件读取的参数来设置一些默认的类属性;或者想根据数据库表中的字段来设置类特性。利用强制方式动态自定义类行为最简单的方法是:首先创建类,然后添加方法和属性。

例如,Anand Pillai(我们熟悉的一个优秀程序员)提出了一个到 Gnosis Utilities 的分包 gnosis.xml.objectify 的路径,该分包就是这么做的。一个专门用来保存 “xml 节点对象” 的叫做 gnosis.xml.objectify._XO_ 的基类就被许多增强的行为 “装饰” 成如下这样:


清单 1. 基类的动态增强

setattr(_XO_, 'orig_tagname', orig_tagname)
setattr(_XO_, 'findelem', findelem)
setattr(_XO_, 'XPath', XPath)
setattr(_XO_, 'change_pcdata', change_pcdata)
setattr(_XO_,'addChild',addChild)

您可能会非常合理地想到,也可以定义 XO 基类的子类来实现同样的增强。在感觉上这是对的,但 Anand 已经提供了 20 多种可能的增强,并且一些特定的用户可能想要其中的一些增强,但不想要 另外一些增强。有太多的替代方法可以轻易地为每种增强情形创建子类。尽管如此,上面的代码未必是恰到好处的。应该用一个附加到 XO、行为是动态决定的自定义元类来完成上述工作。但是这又让我们回到了希望避免的聪明过度(和不透明性)上。

上述问题的一种干净、漂亮的解决方案可能需要向 Python 添加类装饰器。如果拥有这些装饰器,编写的代码可能就像这样:


清单 2. 向 Python 添加类装饰器

features = [('XPath',XPath), ('addChild',addChild), ('is_root',is_root)]
@enhance(features)
class _XO_plus(gnosis.xml.objectify._XO_): pass
gnosis.xml.objectify._XO_ = _XO_plus

然而,目前没有这种语法。




回页首


当元类变复杂时

表面上看本文除了大惊小怪之外,似乎毫无意义。例如,为什么不直接把 XO 元类定义为 Enhance,然后就一切 OK 了呢。 Enhance.__init__() 可以为所讨论的特定用途添加所需的任何功能。可能看起来像这样:


清单 3. 将 XO 定义为 Enhance

class _XO_plus(gnosis.xml.objectify._XO_):
__metaclass__ = Enhance
features = [('XPath',XPath), ('addChild',addChild)]
gnosis.xml.objectify._XO_ = _XO_plus

不幸的是,当考虑到继承时,问题却没有这么简单。一旦为基类定义了一个自定义元类,所有派生类都将继承此元类,所以初始化代码将魔法般隐式地在所有 派生类 上运行。这在特定的情形中可能还不错(例如,假设必须将所有类都注册到您自己的框架中:使用元类可以确保不会忘记注册派生类),然而,在许多情况下则可能 不喜欢这种行为,因为:

  • 您相信显式比隐式更好
  • 派生类具有跟基类相同的动态类属性。为每个派生类 再次设置这些属性是一种浪费,因为通过继承它们就会拥有这些属性。如果初始化代码很慢或者需要大量的计 算,那么这一特性就显得特别重要。也许会在元类代码中添加一个检查,以查看是否在父类中设置了这些属性,但是这样会增加负担,并且不会控制所有的类。
  • 自定义元类将会使类有些不可思议和不标准:您肯定不想增加元类冲突、“ __slots__ ” 问题、跟扩展类( Zope )斗争和其他复杂问题的几率。元类比很多人认识到的更加脆弱。我们甚至在试验代码中用了四年之后还很少在生产代码中使用它们。
  • 您觉得对于类初始化这类简单的工作使用自定义元类是杀鸡用牛刀,所以想要使用一种更为简单的解决方案。

换句话说,只有当想在派生类上运行代码,又不想让用户注意到时,才应该使用自定义元类。如果不属于这种情形,那就跳过元类,使您(和您的用户)的生活更加惬意。




回页首


classinitializer 装饰器

本文以下部分可能会被谴责为聪明过度。但是聪明不应该加重用户的负担,只应该加重我们作者的负担。读者可以做一些与我们假设的理想类装饰器类似的事 情,但 是要避免在元类方法中出现的继承及元类冲突问题。我们后面给出的 “不可思议的” 装饰器通常情况下只能增强直观的(但稍微有些难看的)强制方法,并且跟下面的例子在 “精神上相当”:


清单 4. 强制方法

def Enhance(cls, **kw):
for k, v in kw.iteritems():
setattr(cls, k, v)
class ClassToBeInitialized(object):
pass
Enhance(ClassToBeInitialized, a=1, b=2)

上面的强制增强器并不是很坏。但是也有一些缺馅:它要求重复输入类名称;可读性不够理想,因为类定义和类初始化是分开的 —— 长的类定义可能会漏掉最后一行;并且它会认为首先定义一些内容然后又立即更改是不对的。

classinitializer 装饰器提供了一个说明性解决方案。装饰器将 Enhance(cls,**kw) 转换为一个能够用于类定义中的方法:


清单 5. 基本操作中神奇的装饰器

>>> @classinitializer # add magic to Enhance
... def Enhance(cls, **kw):
... for k, v in kw.iteritems():
... setattr(cls, k, v)
>>> class ClassToBeInitialized(object):
... Enhance(a=1, b=2)
>>> ClassToBeInitialized.a
1
>>> ClassToBeInitialized.b
2

如果使用过 zope 界面,也许见过类初始化器的例子 (zope.interface.implements)。事实上,classinitializer 是使用一个从 Phillip J. Eby 开创的 zope.interface.advice 复制过来的技巧来实现的。此技巧使用 “ __metaclass__ ” 钩子,但是它不使用 自定义类。ClassToBeInitialized 保留了它原始的元类,即新式类的普通内置元类 type


>>> type(ClassToBeInitialized)
<type 'type'>

原则上,此技巧也适用于老式类,并且应该容易编写一个实现来使老式类保持老的样式。然而,由于根据 Guido 所说的 “老式类在精神上是不受赞成的”,当前的实现将老式类转换为新式类:


清单 6. 升级为新式类

>>> class WasOldStyle:
... Enhance(a=1, b=2)
>>> WasOldStyle.a, WasOldStyle.b
(1, 2)
>>> type(WasOldStyle)
<type 'type'>

classinitializer 装饰器的一个动机是要隐藏细节,使一般的人们能够用一种容易的方法实现他们自己的类初始化器,而不必知道类创建工作的细节和_metaclass_ 钩子的秘密。另一个动机是,即使对于 Python 奇才来说,每次编写新的类初始化器时都得重写管理 _metaclass_ 钩子的代码也是很不方便的。

最后应该注意,我们指出 Enhance 的已装饰版本当作类范围外的未装饰版本来运行已经足够漂亮了,假设传递给它一个显式类参数:

>>> Enhance(WasOldStyle, a=2)
>>> WasOldStyle.a
2




回页首


极度不可思议

下面是 classinitializer 的代码。使用装饰器不需要理解该代码:


清单 7. classinitializer 装饰器

import sys
def classinitializer(proc):
# basic idea stolen from zope.interface.advice, P.J. Eby
def newproc(*args, **kw):
frame = sys._getframe(1)
if '__module__' in frame.f_locals and not /
'__module__' in frame.f_code.co_varnames: # we are in a class
if '__metaclass__' in frame.f_locals:
raise SyntaxError("Don't use two class initializers or/n"
"a class initializer together with a __metaclass__ hook")
def makecls(name, bases, dic):
try:
cls = type(name, bases, dic)
except TypeError, e:
if "can't have only classic bases" in str(e):
cls = type(name, bases + (object,), dic)
else: # other strange errs, e.g. __slots__ conflicts
raise
proc(cls, *args, **kw)
return cls
frame.f_locals["__metaclass__"] = makecls
else:
proc(*args, **kw)
newproc.__name__ = proc.__name__
newproc.__module__ = proc.__module__
newproc.__doc__ = proc.__doc__
newproc.__dict__ = proc.__dict__
return newproc

从实现上看,类初始化器是如何工作的就变得很清晰了:当在类中调用一个类初始化器时,实际上定义了一个 _metaclass_ 钩子,它将会被这个类的元类(一般是 type) 调用。元类将创建此类(作为一个新式类)并将其传递给类初始化器过程。

技巧和警告

当类初始化器(重新)定义 _metaclass_ 钩子时,它们不能很好地与显式(与隐式继承的相反)定义 _metaclass_ 钩子的类协作。如果 _metaclass_ 钩子在类初始化器之后 定义,它会静静地 覆盖类初始化器。


清单 8.表项目 index.html主页

>>> class C:
... Enhance(a=1)
... def __metaclass__(name, bases, dic):
... cls = type(name, bases, dic)
... print 'Enhance is silently ignored'
... return cls
...
Enhance is silently ignored
>>> C.a
Traceback (most recent call last):
...
AttributeError: type object 'C' has no attribute 'a'

然而不幸的是,这个问题没有通用的解决方案;我们只是简单地记录。另一方面,如果在 _metaclass_ 钩子之后 调用类初始化器,将会得到异常:


清单 9. 本地元类出现错误

>>> class C:
... def __metaclass__(name, bases, dic):
... cls = type(name, bases, dic)
... print 'calling explicit __metaclass__'
... return cls
... Enhance(a=1)
...
Traceback (most recent call last):
...
SyntaxError: Don't use two class initializers or
a class initializer together with a __metaclass__ hook

出现错误比静静地覆盖显式的 _metaclass_ 钩子要好。因此,如果试图同时使用两个类初始化器,或者两次调用同一个类初始化器,将导致错误:


清单 10. 双重增强导致了一个问题

>>> class C:
... Enhance(a=1)
... Enhance(b=2)
Traceback (most recent call last):
...
SyntaxError: Don't use two class initializers or
a class initializer together with a__metaclass__ hook

从好的方面看,继承的 _metaclass_ 钩子和自定义元类的所有问题都被解决了:


清单 11. 有效地增强继承的元类

>>> class B: # a base class with a custom metaclass
... class __metaclass__(type):
... pass
>>> class C(B): # class with both custom metaclass AND class initializer
... Enhance(a=1)
>>> C.a
1
>>> type(C)
<class '_main.__metaclass__'>

类初始化器并没有干扰到 C 的元类,它继承了基类 B,并且继承的元类不但不会影响到类初始化器,而且会很好地运行。相反,如果试图在基类中直接调用 Enhance,则可能会遇到问题。




回页首


总结

分享这篇文章……

digg 将本文提交到 Digg。
del.icio.us 发布到 del.icio.us。
Slashdot 提交到 Slashdot!

使用所有这些定义的方法,自定义类初始化将变得更加简单和美观。可能就像下面的清单这么简单:


清单 12. 最简形式的增强

class _XO_plus(gnosis.xml.objectify._XO_):
Enhance(XPath=XPath, addChild=addChild, is_root=is_root)
gnosis.xml.objectify._XO_ = _XO_plus

这个例子仍然使用了“注入”,这对普通情况来说有些多余;也就是说,我们将增强的类放回到模块名称空间中的一个特定名称中。这对特定的模块是必要的,但是大多数时候都不需要。在任何情况下,Enhance() 的参数不需要像上面那样固定在代码中,您可以公平地对完全动态的事情使用 Enhance(**feature_set)

另一点需要注意的是,Enhance() 函数的功能远不只上面提到的简单版本。装饰器更擅长完成复杂的增强功能。例如,以下是一个将 “记录” 添加到类的 Enhance() 函数:


清单 13. 类增强的变体

@classinitializer
def def_properties(cls, schema):
"""
Add properties to cls, according to the schema, which is a list
of pairs (fieldname, typecast). A typecast is a
callable converting the field value into a Python type.
The initializer saves the attribute names in a list cls.fields
and the typecasts in a list cls.types. Instances of cls are expected
to have private attributes with names determined by the field names.
"""
cls.fields = []
cls.types = []
for name, typecast in schema:
if hasattr(cls, name): # avoid accidental overriding
raise AttributeError('You are overriding %s!' % name)
def getter(self, name=name):
return getattr(self, '_' + name)
def setter(self, value, name=name, typecast=typecast):
setattr(self, '_' + name, typecast(value))
setattr(cls, name, property(getter, setter))
cls.fields.append(name)
cls.types.append(typecast)

不同之处在于:(a)什么被增强了;(b)这种方法是如何工作的;(c)基类的工作都保持正交:


清单 14. 自定义记录类

>>> class Article(object):
... # fields and types are dynamically set by the initializer
... def_properties([('title', str), ('author', str), ('date', date)])
... def __init__(self, values): # add error checking if you like
... for field, cast, value in zip(self.fields, self.types, values):
... setattr(self, '_' + field, cast(value))

>>> a=Article(['How to use class initializers', 'M. Simionato', '2006-07-10'])
>>> a.title
'How to use class initializers'
>>> a.author
'M. Simionato'
>>> a.date
datetime.date(2006, 7, 10)



参考资料

学习

获得产品和技术
  • 订购 SEK for Linux,两张 DVD,包含最新的用于 Linux 的 IBM 试用软件,这些软件来自 DB2®、Lotus®、Rational®、Tivoli® 和 WebSphere®。
  • 从 developerWorks 下载 IBM 试用软件,构建下一个 Linux 开发项目。

讨论


关于作者

David Mertz

从 2000 年开始,David Mertz 就一直在为 developerWorks 专栏 Charming PythonXML Matters 撰稿。您可以阅读他撰写的书籍 Text Processing in Python有关 David 的更多信息,请访问其 个人主页


评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值