python高级编程第3版_Python中的元类编程,第3部分

python高级编程第3版

去年,我参加了EuroPython 2006会议。 会议很好,组织完善,谈判水平很高,人们非常友善。 尽管如此,我注意到Python社区中令人不安的趋势促使了本文的发展。 几乎同时,我的合著者David Mertz也在思考类似的问题,并提交了一些Gnosis Utilities补丁。 争论的趋势是走向聪明的趋势。 不幸的是,尽管Python社区中的聪明才刚刚被局限在Zope和Twisted中,但如今却无处不在。

在实验项目和学习练习中,我们绝不反对聪明。 我们的牢骚在于我们必须作为用户应对的生产框架中的巧妙之处。 在本文中,我们希望至少在我们拥有一些专业知识的领域(即元类滥用)中,从聪明上做出一点贡献。

对于本文,我们采取无情的态度:我们认为元类滥用是对元类的任何使用,在这种情况下,无需自定义元类就可以很好地解决相同的问题。

最常见的元编程方案之一是创建具有动态生成的属性和方法的类。 与普遍的看法相反,这是一项工作,在大多数情况下,您不需要并且不需要自定义元类。

本文面向两组读者:普通的程序员,虽然他们会从一些元编程技巧中受益,但是却因大脑融化的概念而感到恐惧; 和聪明的程序员,他们太聪明了,应该更了解。 后者的问题是很容易变得聪明,而花很多时间才能变得聪明。 例如,我们花了几个月的时间来了解如何使用元类,但是花了几年的时间来了解如何不使用元类。

关于类初始化

在类创建期间,将一劳永逸地设置类的属性和方法。 或者更确切地说,在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提供了大约二十种可能的增强功能,特定用户可能需要其中一些功能, 而不需要其他功能。 太多的排列无法轻松地为每个增强方案创建子类。 上面的代码仍然不是很漂亮 。 您可以使用附加到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并完成它。 Enhance.__init__()可以愉快地添加特定功能所需的任何功能。 可能看起来像这样:

清单3.将XO定义为增强功能
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是通过使用从zope.interface.advice复制的技巧实现的,该技巧归功于Phillip J. Eby。 该技巧使用“ __metaclass__”钩子,但不使用自定义元类。 ClassToBeInitialized保留其原始的元类,即新样式类的普通内置元类type

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

原则上,该技巧也适用于旧样式类,并且编写一个使旧样式类保持旧样式的实现很容易。 但是,由于根据Guido的说法,“旧样式类在道德上已过时”,因此当前实现将旧样式类转换为新样式类:

清单6.推广到newstyle
>>> 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 ,则会遇到麻烦。

把它放在一起

在定义了所有这些机制之后,自定义类初始化变得非常简单和美观。 它可能很简单:

清单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()函数可以完成的功能远远超过上面建议的简单版本。 装饰者非常乐意调整更复杂的增强功能。 例如,以下是将“记录”添加到类的代码:

清单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)

翻译自: https://www.ibm.com/developerworks/opensource/library/l-pymeta3/index.html

python高级编程第3版

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值