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用户推荐系统曼哈顿算法实现完整代码
Dec 01 Python
Python使用matplotlib绘制随机漫步图
Aug 27 Python
python遍历文件夹找出文件夹后缀为py的文件方法
Oct 21 Python
解决Python2.7中IDLE启动没有反应的问题
Nov 30 Python
pandas DataFrame 交集并集补集的实现
Jun 24 Python
Python实现Selenium自动化Page模式
Jul 14 Python
python使用hdfs3模块对hdfs进行操作详解
Jun 06 Python
Python爬取豆瓣数据实现过程解析
Oct 27 Python
浅谈Python numpy创建空数组的问题
May 25 Python
详解Python中的进程和线程
Jun 23 Python
Python图像处理库PIL详细使用说明
Apr 06 Python
Python使用mitmproxy工具监控手机 下载手机小视频
Apr 18 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
PHP 出现乱码和Sessions验证问题的解决方法!
2008/12/06 PHP
destoon在360浏览器下出现用户被强行注销的解决方法
2014/06/26 PHP
PHP配置把错误日志以邮件方式发送方法(Windows系统)
2015/06/23 PHP
php中实现进程锁与多进程的方法
2016/09/18 PHP
php+redis实现多台服务器内网存储session并读取示例
2017/01/12 PHP
php7连接MySQL实现简易查询程序的方法
2020/10/13 PHP
js 实现无缝滚动 兼容IE和FF
2009/07/15 Javascript
读jQuery之六 缓存数据功能介绍
2011/06/21 Javascript
jquery验证表单中的单选与多选实例
2013/08/18 Javascript
jQuery实现公告新闻自动滚屏效果实例代码
2016/07/14 Javascript
js中获取键盘事件的简单实现方法
2016/10/10 Javascript
js 中获取制定的cook信息实现方法
2016/11/19 Javascript
解决vue 按钮多次点击重复提交数据问题
2018/05/10 Javascript
JavaScript对象拷贝与Object.assign用法实例分析
2018/06/20 Javascript
JavaScript中set与get方法用法示例
2018/08/15 Javascript
用vuex写了一个购物车H5页面的示例代码
2018/12/04 Javascript
vue 解决路由只变化参数页面组件不更新问题
2019/11/05 Javascript
在vue项目中引用Antv G2,以饼图为例讲解
2020/10/28 Javascript
微信小程序抽奖组件的使用步骤
2021/01/11 Javascript
[00:17]天涯墨客一技能展示
2018/08/25 DOTA
python实现代码行数统计示例分享
2014/02/10 Python
python实现随机梯度下降(SGD)
2020/03/24 Python
Python中跳台阶、变态跳台阶与矩形覆盖问题的解决方法
2018/05/19 Python
使用python Fabric动态修改远程机器hosts的方法
2018/10/26 Python
python使用MQTT给硬件传输图片的实现方法
2019/05/05 Python
基于django传递数据到后端的例子
2019/08/16 Python
python nohup 实现远程运行不宕机操作
2020/04/16 Python
Python中生成ndarray实例讲解
2021/02/22 Python
详解CSS3+JS完美实现放大镜模式
2020/12/03 HTML / CSS
学生的自我鉴定范文
2013/10/24 职场文书
家长给老师的道歉信
2014/01/13 职场文书
护士岗前培训自我评鉴
2014/02/28 职场文书
保护水资源的标语
2014/06/17 职场文书
2015年五一劳动节演讲稿
2015/03/18 职场文书
如何理解Vue前后端数据交互与显示
2021/05/10 Vue.js
vue 给数组添加新对象并赋值
2022/04/20 Vue.js