Python 中 Meta Classes详解


Posted in Python onFebruary 13, 2016

接触过 Django 的同学都应该十分熟悉它的 ORM 系统。对于 python 新手而言,这是一项几乎可以被称作“黑科技”的特性:只要你在models.py中随便定义一个Model的子类,Django 便可以:

  1. 获取它的字段定义,并转换成表结构
  2. 读取Meta内部类,并转化成相应的配置信息。对于特殊的Model(如abstract、proxy),还要进行相应的转换
  3. 为没有定义objects的Model加上一个默认的Manager

开发之余,我也曾脑补过其背后的原理。曾经,我认为是这样的:

启动时,遍历models.py中的所有属性,找到Model的子类,并对其进行上述的修改。
当初,我还以为自己触碰到了真理,并曾将其应用到实际生产中——为 SAE 的 KVDB 写了一个类 ORM 系统。然而在实现的过程中,我明显感受到了这种方法的丑陋,而且性能并不出色(因为要遍历所有的定义模块)。

那么事实上,Django 是怎么实现的呢?

自古以来我们制造东西的方法都是“自上而下”的,是用切削、分割、组合的方法来制造。然而,生命是自下而上地,自发地建造起来的,这个过程极为低廉。
——王晋康 《水星播种》

这句话揭示了生命的神奇所在:真正的生命都是由基本物质自发构成的,而非造物主流水线式的加工。

那么,如果 类 也有生命的话,对它自己的修饰就不应该由调用者来完成,而应该是自发的。

幸而,python 提供了造物主的接口——这便是 Meta Classes,或者称为“元类”。

元类 是什么?

简单说:元类就是类的类。

首先,要有一个概念:

python 中,一切都是对象。

没错,一切,包括 类 本身。

既然,类 是 对象,对象 是 类的实例,那么——类 也应该有 类 才对。

类的类:type

在 python 中,我们可以用type检测一个对象的类,如:

print type(1) # <type 'int'>

如果对一个类操作呢?

print type(int) # <type 'type'>

class MyClass(object): pass

print type(MyClass) # <type 'type'>

print type(type) # <type 'type'>

这说明:type其实是一个类型,所有类——包括type自己——的类都是type。

type 简介

从 官方文档 中,我们可以知道:

和 dict 类似,type 也是一个工厂构造函数,调用其将返回 一个type类型的实例(即 类)。
type 有两个重载版本:
+ `type(object)`,即我们最常用的版本。
+ `type(name, bases, dict)`,一个更强大的版本。通过指定 类名称(`name`)、父类列表(`bases`)和 属性字典(`dict`) 动态合成一个类。

下面两个语句等价:

class Integer(int):

  name = 'my integer'

  def increase(self, num):
    return num + 1

  # -------------------

  Integer = type('Integer', (int, ), {
  'name': 'my integer',
  'increase': lambda self, num: \
          num + 1  # 很酷的写法,不是么
  })

也就是说:类的定义过程,其实是type类型实例化的过程。

然而这和修饰一个已定义的类有什么关系呢?

当然有啦~既然“类的定义”就是“type类型的初始化过程”,那其中必定会调用到type的构造函数(__new__() 或 __init__())。只要我们继承 type类 并修改其 __new__函数,在这里面动手脚就可以啦。

接下来我们将通过一个栗子感受 python 的黑魔法,不过在此之前,我们要先了解一个语法糖。

__metaclass__ 属性

有没觉得上面第二段示例有些鬼畜呢?它勒令程序员将类的成员写成一个字典,简直是反人类。如果我们真的是要通过修改 元类 来改变 类 的行为的话,似乎就必须采用这种方法了~~简直可怕~~

好在,python 2.2 时引进了一个语法糖:__metaclass__。

class Integer(int):

  __metaclass__ = IntMeta

现在将会等价于:

Integer = IntMeta('Integer', (int, ), {})

由此一来,我们在使用传统类定义的同时,也可以使用元类啦。

栗子:子类净化器

需求描述

你是一个有语言洁癖的开发者,平时容不得别人讲一句脏话,在开发时也是如此。现在,你写出了一个非常棒的框架,并马上要将它公之于众了。不过,你的强迫症又犯了:如果你的使用者在代码中写满了脏话,怎么办?岂不是玷污了自己的纯洁?
假如你就是这个丧心病狂的开发者,你会怎么做?

在知道元类之前,你可能会无从下手。不过,这个问题你可以用 元类 轻松解决——只要在类定义时过滤掉不干净的字眼就好了(百度贴吧的干活~~)。

我们的元类看起来会是这样的:

sensitive_words_list = ['asshole', 'fuck', 'shit']

def detect_sensitive_words(string):
  '''检测敏感词汇'''
  words_detected = filter(lambda word: word in string.lower(), sensitive_words_list)

  if words_detected:
    raise NameError('Sensitive words {0} detected in the string "{1}".' \
      .format(
        ', '.join(map(lambda s: '"%s"' % s, words_detected)),
        string
      )
    )

class CleanerMeta(type):

  def __new__(cls, class_name, bases, attrs):
    detect_sensitive_words(class_name) # 检查类名
    map(detect_sensitive_words, attrs.iterkeys()) # 检查属性名

    print "Well done! You are a polite coder!" # 如无异常,输出祝贺消息

    return super(CleanerMeta, cls).__new__(cls, class_name, bases, attrs)
    # 重要!这行一定不能漏!!这回调用内建的类构造器来构造类,否则定义好的类将会变成 None
现在,只需这样定义基类:

class APIBase(object):

  __metaclass__ = CleanerMeta

  # ...
那么所有 APIBase 的派生类都会接受安全审查(奸笑~~):

class ImAGoodBoy(APIBase):

  a_polite_attribute = 1

# [Output] Well done! You are a polite coder!

class FuckMyBoss(APIBase):

  pass

# [Output] NameError: Sensitive words "fuck" detected in the string "FuckMyBoss".

class PretendToBePolite(APIBase):

  def __fuck_your_asshole(self):
    pass

# [Output] NameError: Sensitive words "asshole", "fuck" detected in the string "_PretendToBePolite__fuck_your_asshole".

看,即使像最后一个例子中的私有属性也难逃审查,因为它们本质都是相同的。

甚至,你还可以对有问题的属性进行偷偷的修改,比如 让不文明的函数在调用时打出一行警告 等等,这里就不多说了。

元类 在实际开发中的应用

日常开发时,元类 常用吗?

当然,Django 的 ORM 就是一个例子,大名鼎鼎的 SQLAlchemy 也用了这种黑魔法。

此外,在一些小型的库中,也有 元类 的身影。比如 abc(奇怪的名字~~)——这是 python 的一个内建库,用于模拟 抽象基类(Abstract Base Classes)。开发者可以使用 abc.abstractmethod 装饰器,将 指定了 __metaclass__ = abc.ABCMeta 的类的方法定义成 抽象方法,同时这个类也成了 抽象基类,抽象基类是不可实例化的。这便实现了对 抽象基类 的模拟。

倘若你也有需要动态修改类定义的需求,不妨也试试这种“黑魔法”。

小结

  1. 类 也是 对象,所有的类都是type的实例
  2. 元类(Meta Classes)是类的类
  3. __metaclass__ = Meta 是 Meta(name, bases, dict) 的 语法糖
  4. 可以通过重载元类的 __new__ 方法,修改 类定义 的行为
Python 相关文章推荐
Python基于checksum计算文件是否相同的方法
Jul 09 Python
Python随机数用法实例详解【基于random模块】
Apr 18 Python
在python3.5中使用OpenCV的实例讲解
Apr 02 Python
django用户登录和注销的实现方法
Jul 16 Python
python读取有密码的zip压缩文件实例
Feb 08 Python
python初学者,用python实现基本的学生管理系统(python3)代码实例
Apr 10 Python
python实现人工智能Ai抠图功能
Sep 05 Python
python图形用户接口实例详解
Dec 16 Python
python圣诞树编写实例详解
Feb 13 Python
Python3 webservice接口测试代码详解
Jun 23 Python
Python使用grequests并发发送请求的示例
Nov 05 Python
教你利用python实现企业微信发送消息
May 23 Python
教大家使用Python SqlAlchemy
Feb 12 #Python
理解Python垃圾回收机制
Feb 12 #Python
一步步解析Python斗牛游戏的概率
Feb 12 #Python
常用python编程模板汇总
Feb 12 #Python
python黑魔法之参数传递
Feb 12 #Python
python实现井字棋游戏
Mar 30 #Python
python搭建微信公众平台
Feb 09 #Python
You might like
php数字游戏 计算24算法
2012/06/10 PHP
php中this关键字用法分析
2016/12/07 PHP
javascript 无提示关闭窗口脚本
2009/08/17 Javascript
jQuery基础框架浅入剖析
2012/12/27 Javascript
12种不宜使用的Javascript语法整理
2013/11/04 Javascript
javascript对JSON数据排序的3个例子
2014/04/12 Javascript
JavaScript判断浏览器类型的方法
2015/02/10 Javascript
jQuery EasyUI 布局之动态添加tabs标签页
2015/11/18 Javascript
一波JavaScript日期判断脚本分享
2016/03/06 Javascript
ichart.js绘制虚线、平均分虚线效果的实现代码
2016/05/05 Javascript
JS修改地址栏参数实例代码
2016/06/14 Javascript
深入理解React Native原生模块与JS模块通信的几种方式
2017/07/24 Javascript
JavaScript设计模式之装饰者模式定义与应用示例
2018/07/25 Javascript
vue导航栏部分的动态渲染实例
2019/11/01 Javascript
JavaScript随机数的组合问题案例分析
2020/05/16 Javascript
js实现滑动滑块验证登录
2020/07/24 Javascript
[48:35]2018DOTA2亚洲邀请赛 4.1 小组赛 A组加赛 TNC vs Optic
2018/04/03 DOTA
python3.0 字典key排序
2008/12/24 Python
使用优化器来提升Python程序的执行效率的教程
2015/04/02 Python
解决Shell执行python文件,传参空格引起的问题
2018/10/30 Python
浅谈python下tiff图像的读取和保存方法
2018/12/04 Python
pyside+pyqt实现鼠标右键菜单功能
2020/12/08 Python
python用tkinter实现一个简易能进行随机点名的界面
2020/09/27 Python
基于CSS3的CSS 多栏(Multi-column)实现瀑布流源码分享
2014/06/11 HTML / CSS
美国医疗用品、医疗设备和家庭保健用品商店:Medical Supply Depot
2018/07/08 全球购物
儿科主治医生个人求职信
2013/09/23 职场文书
仪器仪表检测毕业生自荐信
2013/10/31 职场文书
总监职责范文
2013/11/09 职场文书
客服主管岗位职责
2013/12/13 职场文书
8和9的加减法教学反思
2014/05/01 职场文书
希特勒的演讲稿
2014/05/23 职场文书
药品营销专业毕业生自荐信
2014/07/02 职场文书
公司领导班子民主生活会对照检查材料
2014/10/02 职场文书
预备党员考察表党小组意见
2015/06/01 职场文书
如何用Node.js编写内存效率高的应用程序
2021/04/30 Javascript
MySQL库表名大小写的选择
2021/06/05 MySQL