Python中的元类编程入门指引


Posted in Python onApril 15, 2015

回顾面向对象编程

让我们先用 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 一直都有元类。但元类中所涉及的方法在 Python 2.2 中才得以更好地公开在人们面前。Python V2.2 明确地不再只使用一个特殊的(通常是隐藏的)元类来创建每个类对象。现在程序员可以创建原始元类 type 的子类,甚至可以用各种元类动态地生成类。当然,仅仅因为 可以在 Python 2.2 中操作元类,这并不能说明您可能想这样做的原因。

而且,不需要使用定制元类来操作类的生成。一种不太费脑筋的概念是 类工厂:一种普通的函数,它可以 返回在函数体内动态创建的类。用传统的 Python 语法,您可以编写:
清单 1. 老式的 Python 1.5.2 类工厂

Python 1.5.2 (#0, Jun 27 1999, 11:23:01) [...]
Copyright 1991-1995 Stichting Mathematisch Centrum, Amsterdam
>>> def class_with_method(func):
...   class klass: pass
...   setattr(klass, func.__name__, func)
...   return klass
...
>>> def say_foo(self): print 'foo'
...
>>> Foo = class_with_method(say_foo)
>>> foo = Foo()
>>> foo.say_foo()
foo

工厂函数 class_with_method() 动态地创建一个类,并返回该类,这个类包含传递给该工厂 的方法/函数。在返回该类之前,在函数体内操作类自身。 new 模块提供了更简洁的编码方式,但其中的选项与 类工厂体内定制代码的选项不同,例如:
清单 2. new 模块中的类工厂

>>> from new import classobj
>>> Foo2 = classobj('Foo2',(Foo,),{'bar':lambda self:'bar'})
>>> Foo2().bar()
'bar'
>>> Foo2().say_foo()
foo

在所有这些情形中,没有将类( Foo 和 Foo2 )的行为直接编写为代码, 而是用动态参数在运行时调用函数来创建类的行为。这里要强调的一点是,不仅 实例可以动态地创建,而且 类本身也可以动态地创建。

元类:寻求问题的解决方案?

    元类的魔力是如此之大,以至于 99% 的用户曾有过的顾虑都是不必要的。如果您想知道是否需要它们,则可以不用它们(那些实际需要元类的人们确实清楚自己需要它们,不需要解释原因)。— Python 专家 Tim Peters

(类的)方法象普通函数一样可以返回对象。所以从这个意义上讲,类工厂可以是类,就象它们可以是函数一样容易,这是显然的。尤其 是 Python 2.2+ 提供了一个称为 type 的特殊类,它正是这样的类工厂。当然,读者会认识到 type() 不象 Python 老版本的内置函数那样“野心勃勃”— 幸运的是,老版本的 type() 函数的行为是由 type 类维护的(换句话说, type(obj) 返回对象 obj 的类型/类)。作为类工厂的新 type 类,其工作方式与函数 new.classobj 一直所具有的方式相同:
清单 3. 作为类工厂元类的 type

>>> X = type('X',(),{'foo':lambda self:'foo'})
>>> X, X().foo()
(<class '__main__.X'>, 'foo')

但是因为 type 现在是(元)类,所以可以自由用它来创建子类:
清单 4. 作为类工厂的 type 后代

>>> class ChattyType(type):
...   def __new__(cls, name, bases, dct):
...     print "Allocating memory for class", name
...     return type.__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':lambda self:'foo'})
Allocating memory for class X
Init'ing (configuring) class X
>>> X, X().foo()
(<class '__main__.X'>, 'foo')

富有“魔力”的 .__new__() 和 .__init__() 方法很特殊,但在概念上,对于任何其它类,它们的工作方式都是一样的。 .__init__() 方法使您能配置所创建的对象; .__new__() 方法使您能定制它的分配。当然,后者没有被广泛地使用,但对于每个 Python 2.2 new 样式的类(通常通过继承而不是覆盖),都存在该方法。

需要注意 type 后代的一个特性;它常使第一次使用元类的人们“上圈套”。按照惯例,这些方法的第一个参数名为 cls ,而不是 self ,因为这些方法是在 已生成的类上进行操作的,而不是在元类上。事实上,关于这点没什么特别的;所有方法附加在它们的实例上,而且元类的实例是类。非特殊的 名称使这更明显:
清单 5. 将类方法附加在所生成的类上

>>> class Printable(type):
...   def whoami(cls): print "I am a", cls.__name__
...
>>> Foo = Printable('Foo',(),{})
>>> Foo.whoami()
I am a Foo
>>> Printable.whoami()
Traceback (most recent call last):
TypeError: unbound method whoami() [...]

所有这些令人惊讶但又常见的做法以及便于掌握的语法使得元类的使用更容易,但也让新用户感到迷惑。对于其它语法有几个元素。但这些新变体的解析顺序需要点技巧。类可以从其祖先那继承元类 — 请注意,这与将元类 作为祖先 不一样(这是另一处常让人迷惑的地方)。对于老式类,定义一个全局 _metaclass_ 变量可以强制使用定制元类。但大多数时间,最安全的方法是,在希望通过定制元类来创建类时,设置该类的 _metaclass_ 类属性。必须在类定义本身中设置变量,因为 如果稍后(在已经创建类对象之后)设置属性 ,则不会使用元类。例如:
清单 6. 用类属性设置元类

>>> class Bar:
...   __metaclass__ = Printable
...   def foomethod(self): print 'foo'
...
>>> Bar.whoami()
I am a Bar
>>> Bar().foomethod()
foo

用这种“魔力”来解决问题

至此,我们已经了解了一些有关元类的基本知识。但要使用元类,则比较复杂。使用元类的困难之处在于,通常在 OOP 设计中,类其实 做得不多。对于封装和打包数据和方法,类的继承结构很有用,但在具体 情形中,人们通常使用实例。

我们认为元类在两大类编程任务中确实有用。

第一类(可能是更常见的一类)是在设计时不能 确切地知道类需要做什么。显然,您对它有所了解,但某个特殊的细节 可能取决于稍后才能得到的信息。“稍后”本身有两类:(a)当应用程序使用库模块时;(b)在运行时,当某种情形存在时。这类很接近于通常所说的“面向方面的编程(Aspect-Oriented Programming,AOP)”。我们将展示一个我们认为非常别致的示例:
清单 7. 运行时的元类配置

% cat dump.py
#!/usr/bin/python
import sys
if len(sys.argv) > 2:
  module, metaklass = sys.argv[1:3]
  m = __import__(module, globals(), locals(), [metaklass])
  __metaclass__ = getattr(m, metaklass)
class Data:
  def __init__(self):
    self.num = 38
    self.lst = ['a','b','c']
    self.str = 'spam'
  dumps  = lambda self: `self`
  __str__ = lambda self: self.dumps()
data = Data()
print data
% dump.py
<__main__.Data instance at 1686a0>

正如您所期望的,该应用程序打印出 data 对象相当常规的描述(常规的实例对象)。但如果将 运行时参数传递给应用程序,则可以得到相当不同的结果:
清单 8. 添加外部序列化元类

% dump.py gnosis.magic MetaXMLPickler
<?xml version="1.0"?>
<!DOCTYPE PyObject SYSTEM "PyObjects.dtd">
<PyObject module="__main__" class="Data" id="720748">
<attr name="lst" type="list" id="980012" >
 <item type="string" value="a" />
 <item type="string" value="b" />
 <item type="string" value="c" />
</attr>
<attr name="num" type="numeric" value="38" />
<attr name="str" type="string" value="spam" />
</PyObject>

这个特殊的示例使用 gnosis.xml.pickle 的序列化样式,但最新的 gnosis.magic 包还包含元类序列化器 MetaYamlDump 、 MetaPyPickler 和 MetaPrettyPrint 。而且, dump.py “应用程序”的用户可以从任何定义了任何期望的 MetaPickler 的 Python 包中利用该“MetaPickler”。出于此目的而 编写合适的元类如下所示:
清单 9. 用元类添加属性

class MetaPickler(type):
  "Metaclass for gnosis.xml.pickle serialization"
  def __init__(cls, name, bases, dict):
    from gnosis.xml.pickle import dumps
    super(MetaPickler, cls).__init__(name, bases, dict)
    setattr(cls, 'dumps', dumps)

这种安排的过人之处在于应用程序程序员不需要了解要使用哪种序列化 — 甚至不需要了解是否 在命令行添加序列化或其它一些跨各部分的能力。

也许元类最常见的用法与 MetaPickler 类似:添加、删除、重命名或替换所产生类中定义的方法。在我们的示例中,在创建类 Data (以及由此再创建随后的每个实例)时,“本机” Data.dump() 方法被应用程序之外的某个方法所替代。


使用这种“魔力”来解决问题的其它方法

存在着这样的编程环境:类往往比实例更重要。例如, 说明性迷你语言(declarative mini-languages)是 Python 库,在类声明中直接表示了它的程序逻辑。David 在其文章“ Create declarative mini-languages”中研究了此问题。在这种情形下,使用元类来影响类创建过程是相当有用的。

一种基于类的声明性框架是 gnosis.xml.validity 。 在此框架下,可以声明许多“有效性类”,这些类表示了一组有关有效 XML 文档的约束。这些声明非常接近于 DTD 中所包含的那些声明。例如,可以用以下代码来配置一篇“dissertation”文档:
清单 10. simple_diss.py gnosis.xml.validity 规则

from gnosis.xml.validity import *
class figure(EMPTY):   pass
class _mixedpara(Or):   _disjoins = (PCDATA, figure)
class paragraph(Some):  _type = _mixedpara
class title(PCDATA):   pass
class _paras(Some):    _type = paragraph
class chapter(Seq):    _order = (title, _paras)
class dissertation(Some): _type = chapter

如果在没有正确组件子元素的情形下尝试实例化 dissertation 类,则会产生一个描述性异常;对于每个 子元素,亦是如此。当只有一种明确的方式可以将参数“提升”为正确的类型 时,会从较简单的参数来生成正确的子元素。

即使有效性类常常(非正式)基于预先存在的 DTD,这些类的实例也还是将自己打印成简单的 XML 文档片段,例如:
清单 11. 基本的有效性类文档的创建

>>> from simple_diss import *
>>> ch = LiftSeq(chapter, ('It Starts','When it began'))
>>> print ch
<chapter><title>It Starts</title>
<paragraph>When it began</paragraph>
</chapter>

通过使用元类来创建有效性类,我们可以从类声明中生成 DTD(我们在这样做的同时,可以向这些有效性类额外添加一个方法):
清单 12. 在模块导入期间利用元类

>>> from gnosis.magic import DTDGenerator, \
...             import_with_metaclass, \
...             from_import
>>> d = import_with_metaclass('simple_diss',DTDGenerator)
>>> from_import(d,'**')
>>> ch = LiftSeq(chapter, ('It Starts','When it began'))
>>> print ch.with_internal_subset()
<?xml version='1.0'?>
<!DOCTYPE chapter [
<!ELEMENT figure EMPTY>
<!ELEMENT dissertation (chapter)+>
<!ELEMENT chapter (title,paragraph+)>
<!ELEMENT title (#PCDATA)>
<!ELEMENT paragraph ((#PCDATA|figure))+>
]>
<chapter><title>It Starts</title>
<paragraph>When it began</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()

def import_with_metaclass(modname, metaklass):
  "Module importer substituting custom metaclass"
  class Meta(object): __metaclass__ = metaklass
  dct = {'__module__':modname}
  mod = __import__(modname)
  for key, val in mod.__dict__.items():
    if inspect.isclass(val):
      setattr(mod, key, type(key,(val,Meta),dct))
  return mod

在这个函数中值得注意的样式是,用指定的元类生成普通的类 Meta 。但是,一旦将 Meta 作为祖先添加之后,也用定制元类来生成它的后代。原则上,象 Meta 这样的类 既可以带有元类生成器(metaclass producer) 也可以带有一组可继承的方法 — Meta 类的这两个方面是无关的。

Python 相关文章推荐
python中常用检测字符串相关函数汇总
Apr 15 Python
Python使用matplotlib绘制动画的方法
May 20 Python
用Python的Django框架来制作一个RSS阅读器
Jul 22 Python
Python数据结构之翻转链表
Feb 25 Python
python WindowsError的错误代码详解
Jul 23 Python
python构建基础的爬虫教学
Dec 23 Python
利用python脚本如何简化jar操作命令
Feb 24 Python
Python 类属性与实例属性,类对象与实例对象用法分析
Sep 20 Python
如何使用python实现模拟鼠标点击
Jan 06 Python
python全局变量引用与修改过程解析
Jan 07 Python
Keras中的多分类损失函数用法categorical_crossentropy
Jun 11 Python
OpenCV-Python实现轮廓拟合
Jun 08 Python
在Python中进行自动化单元测试的教程
Apr 15 #Python
pygame学习笔记(6):完成一个简单的游戏
Apr 15 #Python
pygame学习笔记(5):游戏精灵
Apr 15 #Python
pygame学习笔记(4):声音控制
Apr 15 #Python
Python的Django框架使用入门指引
Apr 15 #Python
python中xrange用法分析
Apr 15 #Python
pygame学习笔记(3):运动速率、时间、事件、文字
Apr 15 #Python
You might like
极典R601SW收音机
2021/03/02 无线电
php xml-rpc远程调用
2008/12/19 PHP
PHP中文处理 中文字符串截取(mb_substr)和获取中文字符串字数
2011/11/10 PHP
PHP gbk环境下json_dencode传送来的汉字
2012/11/13 PHP
隐藏Nginx或Apache以及PHP的版本号的方法
2016/01/03 PHP
PHP基于单例模式编写PDO类的方法
2016/09/13 PHP
Laravel使用模型实现like模糊查询的例子
2019/10/24 PHP
学习ExtJS accordion布局
2009/10/08 Javascript
Javascript select下拉框操作常用方法
2009/11/09 Javascript
用Jquery重写windows.alert方法实现思路
2013/04/03 Javascript
js Array对象的扩展函数代码
2013/04/24 Javascript
js导出table数据到excel即导出为EXCEL文档的方法
2013/10/10 Javascript
Javascript递归打印Document层次关系实例分析
2015/05/15 Javascript
javascript正则表达式总结
2016/02/29 Javascript
JQuery的常用选择器、过滤器、方法全面介绍
2016/05/25 Javascript
JS实现太极旋转思路分析
2016/12/09 Javascript
Angular2 Service实现简单音乐播放器服务
2017/02/24 Javascript
详解Node.js 中使用 ECDSA 签名遇到的坑
2018/11/26 Javascript
JS中的算法与数据结构之字典(Dictionary)实例详解
2019/08/20 Javascript
详细介绍解决vue和jsp结合的方法
2020/02/06 Javascript
Vue项目结合Vue-layer实现弹框式编辑功能(实例代码)
2020/03/11 Javascript
[03:08]TI9战队档案 - Vici Gaming
2019/08/20 DOTA
[01:16:50]DOTA2-DPC中国联赛 正赛 Phoenix vs CDEC BO3 第一场 3月7日
2021/03/11 DOTA
教你如何在Django 1.6中正确使用 Signal
2014/06/22 Python
对python list 遍历删除的正确方法详解
2018/06/29 Python
python 统计数组中元素出现次数并进行排序的实例
2018/07/02 Python
python3 中文乱码与默认编码格式设定方法
2018/10/31 Python
Python面向对象程序设计类变量与成员变量、类方法与成员方法用法分析
2019/04/12 Python
Python GUI之tkinter窗口视窗教程大集合(推荐)
2020/10/20 Python
Dockers美国官方网站:卡其裤、男士服装、鞋及配件
2016/11/22 全球购物
美国亚马逊旗下男装网站:East Dane(支持中文)
2019/09/25 全球购物
Gerry Weber德国官网:优质女性时装,德国最大的时装公司之一
2019/11/02 全球购物
必须要使用游标的SQL语句有那些
2012/05/07 面试题
法人代表委托书
2014/04/04 职场文书
自主招生教师推荐信
2014/05/10 职场文书
浅析Redis Sentinel 与 Redis Cluster
2021/06/24 Redis