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中条件选择和循环语句使用方法介绍
Mar 13 Python
有关wxpython pyqt内存占用问题分析
Jun 09 Python
Python字符串和文件操作常用函数分析
Apr 08 Python
介绍Python的Django框架中的QuerySets
Apr 20 Python
Python中type的构造函数参数含义说明
Jun 21 Python
Python实现的爬虫功能代码
Jun 24 Python
python使用tkinter库实现五子棋游戏
Jun 18 Python
python实现月食效果实例代码
Jun 18 Python
Python生命游戏实现原理及过程解析(附源代码)
Aug 01 Python
导入tensorflow时报错:cannot import name 'abs'的解决
Oct 10 Python
Python 实现RSA加解密文本文件
Dec 30 Python
详解用selenium来下载小姐姐图片并保存
Jan 26 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翻页跳转功能实现方法
2020/11/30 PHP
利用Fix Rss Feeds插件修复WordPress的Feed显示错误
2015/12/19 PHP
php基于dom实现读取图书xml格式数据的方法
2017/02/03 PHP
PHP基于堆栈实现的高级计算器功能示例
2017/09/15 PHP
PHP实现网站应用微信登录功能详解
2019/04/11 PHP
用 Javascript 验证表单(form)中的单选(radio)值
2009/09/08 Javascript
Jquery搜索父元素操作方法
2015/02/10 Javascript
javascript实现淘宝幻灯片广告展示效果
2015/04/27 Javascript
javascript实现点击单选按钮链接转向对应网址的方法
2015/08/12 Javascript
JavaScript学习小结(7)之JS RegExp
2015/11/29 Javascript
javascript ES6 新增了let命令使用介绍
2017/07/07 Javascript
seajs下require书写约定实例分析
2018/05/16 Javascript
微信小程序按钮点击跳转页面详解
2019/05/06 Javascript
Django返回json数据用法示例
2016/09/18 Python
python爬取哈尔滨天气信息
2018/07/14 Python
Python3 利用requests 库进行post携带账号密码请求数据的方法
2018/10/26 Python
python爬虫租房信息在地图上显示的方法
2019/05/13 Python
Django 重写用户模型的实现
2019/07/29 Python
Django之路由层的实现
2019/09/09 Python
Python将视频或者动态图gif逐帧保存为图片的方法
2019/09/10 Python
python验证码图片处理(二值化)
2019/11/01 Python
Python实现图像的垂直投影示例
2020/01/17 Python
python下对hsv颜色空间进行量化操作
2020/06/04 Python
利用Python将多张图片合成视频的实现
2020/11/23 Python
Python OpenCV中的numpy与图像类型转换操作
2020/12/11 Python
深入浅析css3 border-image边框图像详解
2015/11/24 HTML / CSS
前后端结合实现amazeUI分页效果
2020/08/21 HTML / CSS
中国综合性网上购物商城:当当(网上卖书起家)
2016/11/16 全球购物
美国户外运动商店:Sun & Ski
2018/08/23 全球购物
专科毕业生学习生活的自我评价
2013/10/26 职场文书
公司活动邀请函
2014/01/24 职场文书
交警个人先进事迹材料
2014/05/11 职场文书
村容村貌整治方案
2014/05/21 职场文书
检讨书1000字
2014/10/11 职场文书
SpringCloud之@FeignClient()注解的使用方式
2021/09/25 Java/Android
Python 操作pdf pdfplumber读取PDF写入Exce
2022/08/14 Python