Python 中的元类编程

来源:互联网 发布:下沙大数据培训 编辑:程序博客网 时间:2024/05/20 22:55
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 也在思考一个类似的关于一些提交给 GnosisUtilities 的补丁程序的问题。这种有争议的趋势就是趋向于耍小聪明。不幸的是,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)



参考资料

学习
  • 您可以参阅本文在 developerWorks 全球站点上的 英文原文。
  • “Python 中的元类编程 -- 将面向对象编程推向新的高度” 的第 1 部分(developerWorks,2003 年 2 月)介绍了元类编程概念和面向对象概念。
  • “Python 中的元类编程,第 2 部分 -- 理解继承的奥秘和实例创建”(developerWorks,2003 年 8 月)更加深入地讨论了 Python 元类的一些细节。
  • “可爱的 Python: Decorator 简化元编程”(developerWorks,2006 年 12 月)介绍了 Python 最新的元编程功能。
  • 查看实现所有功能的 代码。
  • 阅读 Michele 的 “A simple and useful doctester for your documentation”。
  • 在 developerWorks Linux 专区 中,可以找到更多的 Linux 开发人员的资源,浏览 最流行的文章和教程。
  • 在 developerWorks 上查看所有的 Linux 技巧 和 Linux 教程。
  • 随时关注 developerWorks 技术活动和网络广播。

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

讨论
  • 通过参与 新 developerWorks 空间 中的博客、论坛、podcasts 和社区,加入 developerWorks 社区。


关于作者

David Mertz

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


类别:Python Django | 添加到搜藏| 浏览(58)| 评论 (1)
 
上一篇:GmailFS - Gmail Filesystem Ins...    下一篇:Linux Net configure manual
原创粉丝点击