在Python中使用元类的教程


Posted in Python onApril 28, 2015

type()

动态语言和静态语言最大的不同,就是函数和类的定义,不是编译时定义的,而是运行时动态创建的。

比方说我们要定义一个Hello的class,就写一个hello.py模块:

class Hello(object):
  def hello(self, name='world'):
    print('Hello, %s.' % name)

当Python解释器载入hello模块时,就会依次执行该模块的所有语句,执行结果就是动态创建出一个Hello的class对象,测试如下:

>>> from hello import Hello
>>> h = Hello()
>>> h.hello()
Hello, world.
>>> print(type(Hello))
<type 'type'>
>>> print(type(h))
<class 'hello.Hello'>

type()函数可以查看一个类型或变量的类型,Hello是一个class,它的类型就是type,而h是一个实例,它的类型就是class Hello。

我们说class的定义是运行时动态创建的,而创建class的方法就是使用type()函数。

type()函数既可以返回一个对象的类型,又可以创建出新的类型,比如,我们可以通过type()函数创建出Hello类,而无需通过class Hello(object)...的定义:

>>> def fn(self, name='world'): # 先定义函数
...   print('Hello, %s.' % name)
...
>>> Hello = type('Hello', (object,), dict(hello=fn)) # 创建Hello class
>>> h = Hello()
>>> h.hello()
Hello, world.
>>> print(type(Hello))
<type 'type'>
>>> print(type(h))
<class '__main__.Hello'>

要创建一个class对象,type()函数依次传入3个参数:

  1.     class的名称;
  2.     继承的父类集合,注意Python支持多重继承,如果只有一个父类,别忘了tuple的单元素写法;
  3.     class的方法名称与函数绑定,这里我们把函数fn绑定到方法名hello上。

通过type()函数创建的类和直接写class是完全一样的,因为Python解释器遇到class定义时,仅仅是扫描一下class定义的语法,然后调用type()函数创建出class。

正常情况下,我们都用class Xxx...来定义类,但是,type()函数也允许我们动态创建出类来,也就是说,动态语言本身支持运行期动态创建类,这和静态语言有非常大的不同,要在静态语言运行期创建类,必须构造源代码字符串再调用编译器,或者借助一些工具生成字节码实现,本质上都是动态编译,会非常复杂。
metaclass

除了使用type()动态创建类以外,要控制类的创建行为,还可以使用metaclass。

metaclass,直译为元类,简单的解释就是:

当我们定义了类以后,就可以根据这个类创建出实例,所以:先定义类,然后创建实例。

但是如果我们想创建出类呢?那就必须根据metaclass创建出类,所以:先定义metaclass,然后创建类。

连接起来就是:先定义metaclass,就可以创建类,最后创建实例。

所以,metaclass允许你创建类或者修改类。换句话说,你可以把类看成是metaclass创建出来的“实例”。

metaclass是Python面向对象里最难理解,也是最难使用的魔术代码。正常情况下,你不会碰到需要使用metaclass的情况,所以,以下内容看不懂也没关系,因为基本上你不会用到。

我们先看一个简单的例子,这个metaclass可以给我们自定义的MyList增加一个add方法:

定义ListMetaclass,按照默认习惯,metaclass的类名总是以Metaclass结尾,以便清楚地表示这是一个metaclass:

# metaclass是创建类,所以必须从`type`类型派生:
class ListMetaclass(type):
  def __new__(cls, name, bases, attrs):
    attrs['add'] = lambda self, value: self.append(value)
    return type.__new__(cls, name, bases, attrs)

 
class MyList(list):
  __metaclass__ = ListMetaclass # 指示使用ListMetaclass来定制类

当我们写下__metaclass__ = ListMetaclass语句时,魔术就生效了,它指示Python解释器在创建MyList时,要通过ListMetaclass.__new__()来创建,在此,我们可以修改类的定义,比如,加上新的方法,然后,返回修改后的定义。

__new__()方法接收到的参数依次是:

  •     当前准备创建的类的对象;
  •     类的名字;
  •     类继承的父类集合;
  •     类的方法集合。

测试一下MyList是否可以调用add()方法:

>>> L = MyList()
>>> L.add(1)
>>> L
[1]

而普通的list没有add()方法:

>>> l = list()
>>> l.add(1)
Traceback (most recent call last):
 File "<stdin>", line 1, in <module>
AttributeError: 'list' object has no attribute 'add'

动态修改有什么意义?直接在MyList定义中写上add()方法不是更简单吗?正常情况下,确实应该直接写,通过metaclass修改纯属变态。

但是,总会遇到需要通过metaclass修改类定义的。ORM就是一个典型的例子。

ORM全称“Object Relational Mapping”,即对象-关系映射,就是把关系数据库的一行映射为一个对象,也就是一个类对应一个表,这样,写代码更简单,不用直接操作SQL语句。

要编写一个ORM框架,所有的类都只能动态定义,因为只有使用者才能根据表的结构定义出对应的类来。

让我们来尝试编写一个ORM框架。

编写底层模块的第一步,就是先把调用接口写出来。比如,使用者如果使用这个ORM框架,想定义一个User类来操作对应的数据库表User,我们期待他写出这样的代码:

class User(Model):
  # 定义类的属性到列的映射:
  id = IntegerField('id')
  name = StringField('username')
  email = StringField('email')
  password = StringField('password')

 
# 创建一个实例:
u = User(id=12345, name='Michael', email='test@orm.org', password='my-pwd')
# 保存到数据库:
u.save()

其中,父类Model和属性类型StringField、IntegerField是由ORM框架提供的,剩下的魔术方法比如save()全部由metaclass自动完成。虽然metaclass的编写会比较复杂,但ORM的使用者用起来却异常简单。

现在,我们就按上面的接口来实现该ORM。

首先来定义Field类,它负责保存数据库表的字段名和字段类型:

class Field(object):
  def __init__(self, name, column_type):
    self.name = name
    self.column_type = column_type
  def __str__(self):
    return '<%s:%s>' % (self.__class__.__name__, self.name)

在Field的基础上,进一步定义各种类型的Field,比如StringField,IntegerField等等:

class StringField(Field):
  def __init__(self, name):
    super(StringField, self).__init__(name, 'varchar(100)')

class IntegerField(Field):
  def __init__(self, name):
    super(IntegerField, self).__init__(name, 'bigint')

下一步,就是编写最复杂的ModelMetaclass了:

class ModelMetaclass(type):
  def __new__(cls, name, bases, attrs):
    if name=='Model':
      return type.__new__(cls, name, bases, attrs)
    mappings = dict()
    for k, v in attrs.iteritems():
      if isinstance(v, Field):
        print('Found mapping: %s==>%s' % (k, v))
        mappings[k] = v
    for k in mappings.iterkeys():
      attrs.pop(k)
    attrs['__table__'] = name # 假设表名和类名一致
    attrs['__mappings__'] = mappings # 保存属性和列的映射关系
    return type.__new__(cls, name, bases, attrs)

以及基类Model:

class Model(dict):
  __metaclass__ = ModelMetaclass

  def __init__(self, **kw):
    super(Model, self).__init__(**kw)

  def __getattr__(self, key):
    try:
      return self[key]
    except KeyError:
      raise AttributeError(r"'Model' object has no attribute '%s'" % key)

  def __setattr__(self, key, value):
    self[key] = value

  def save(self):
    fields = []
    params = []
    args = []
    for k, v in self.__mappings__.iteritems():
      fields.append(v.name)
      params.append('?')
      args.append(getattr(self, k, None))
    sql = 'insert into %s (%s) values (%s)' % (self.__table__, ','.join(fields), ','.join(params))
    print('SQL: %s' % sql)
    print('ARGS: %s' % str(args))

当用户定义一个class User(Model)时,Python解释器首先在当前类User的定义中查找__metaclass__,如果没有找到,就继续在父类Model中查找__metaclass__,找到了,就使用Model中定义的__metaclass__的ModelMetaclass来创建User类,也就是说,metaclass可以隐式地继承到子类,但子类自己却感觉不到。

在ModelMetaclass中,一共做了几件事情:

  •     排除掉对Model类的修改;
  •     在当前类(比如User)中查找定义的类的所有属性,如果找到一个Field属性,就把它保存到一个__mappings__的dict中,同时从类属性中删除该Field属性,否则,容易造成运行时错误;
  •     把表名保存到__table__中,这里简化为表名默认为类名。

在Model类中,就可以定义各种操作数据库的方法,比如save(),delete(),find(),update等等。

我们实现了save()方法,把一个实例保存到数据库中。因为有表名,属性到字段的映射和属性值的集合,就可以构造出INSERT语句。

编写代码试试:

u = User(id=12345, name='Michael', email='test@orm.org', password='my-pwd')
u.save()

输出如下:

Found model: User
Found mapping: email ==> <StringField:email>
Found mapping: password ==> <StringField:password>
Found mapping: id ==> <IntegerField:uid>
Found mapping: name ==> <StringField:username>
SQL: insert into User (password,email,username,uid) values (?,?,?,?)
ARGS: ['my-pwd', 'test@orm.org', 'Michael', 12345]

可以看到,save()方法已经打印出了可执行的SQL语句,以及参数列表,只需要真正连接到数据库,执行该SQL语句,就可以完成真正的功能。

不到100行代码,我们就通过metaclass实现了一个精简的ORM框架,完整的代码从这里下载:

https://github.com/michaelliao/learn-python/blob/master/metaclass/simple_orm.py

最后解释一下类属性和实例属性。直接在class中定义的是类属性:

class Student(object):
  name = 'Student'

实例属性必须通过实例来绑定,比如self.name = 'xxx'。来测试一下:

>>> # 创建实例s:
>>> s = Student()
>>> # 打印name属性,因为实例并没有name属性,所以会继续查找class的name属性:
>>> print(s.name)
Student
>>> # 这和调用Student.name是一样的:
>>> print(Student.name)
Student
>>> # 给实例绑定name属性:
>>> s.name = 'Michael'
>>> # 由于实例属性优先级比类属性高,因此,它会屏蔽掉类的name属性:
>>> print(s.name)
Michael
>>> # 但是类属性并未消失,用Student.name仍然可以访问:
>>> print(Student.name)
Student
>>> # 如果删除实例的name属性:
>>> del s.name
>>> # 再次调用s.name,由于实例的name属性没有找到,类的name属性就显示出来了:
>>> print(s.name)
Student

因此,在编写程序的时候,千万不要把实例属性和类属性使用相同的名字。

在我们编写的ORM中,ModelMetaclass会删除掉User类的所有类属性,目的就是避免造成混淆。

 

Python 相关文章推荐
Python中的变量和作用域详解
Jul 13 Python
python如何通过twisted实现数据库异步插入
Mar 20 Python
Python for循环与range函数的使用详解
Mar 23 Python
解决python3中的requests解析中文页面出现乱码问题
Apr 19 Python
Django框架之DRF 基于mixins来封装的视图详解
Jul 23 Python
基于python分析你的上网行为 看看你平时上网都在干嘛
Aug 13 Python
使用python实现unix2dos和dos2unix命令的例子
Aug 13 Python
Pytorch.nn.conv2d 过程验证方式(单,多通道卷积过程)
Jan 03 Python
解决Jupyter Notebook使用parser.parse_args出现错误问题
Apr 20 Python
Scrapy中如何向Spider传入参数的方法实现
Sep 28 Python
Python self用法详解
Nov 28 Python
pytorch 两个GPU同时训练的解决方案
Jun 01 Python
python删除列表中重复记录的方法
Apr 28 #Python
python3实现短网址和数字相互转换的方法
Apr 28 #Python
python实现从网络下载文件并获得文件大小及类型的方法
Apr 28 #Python
浅析Python中的多重继承
Apr 28 #Python
python输出当前目录下index.html文件路径的方法
Apr 28 #Python
Python实现基于权重的随机数2种方法
Apr 28 #Python
python使用urllib2实现发送带cookie的请求
Apr 28 #Python
You might like
风味层面去分析咖啡油脂
2021/03/03 咖啡文化
PHP获取php,mysql,apche的版本信息示例代码
2014/01/16 PHP
PHP防范SQL注入的具体方法详解(测试通过)
2014/05/09 PHP
PHP连接MySQL进行增、删、改、查操作
2017/02/19 PHP
Laravel框架使用Redis的方法详解
2018/05/30 PHP
PHP getName()函数讲解
2019/02/03 PHP
php的instanceof和判断闭包Closure操作示例
2020/01/26 PHP
实例讲解PHP表单
2020/06/10 PHP
Avengerls vs Newbee BO3 第二场2.18
2021/03/10 DOTA
AJAX架构之Dojo篇
2007/04/10 Javascript
jQuery图片播放8款精美插件分享
2013/02/17 Javascript
JQuery的自定义事件代码,触发,绑定简单实例
2013/08/01 Javascript
js如何取消事件冒泡
2013/09/23 Javascript
Javascript实现滚动图片新闻的实例代码
2013/11/27 Javascript
JavaScript原生对象之Date对象的属性和方法详解
2015/03/13 Javascript
jquery实现的点击翻书效果代码
2015/11/04 Javascript
zTree获取当前节点的下一级子节点数实例
2017/09/05 Javascript
jQuery插件实现非常实用的tab栏切换功能【案例】
2019/02/18 jQuery
jQuery属性选择器用法实例分析
2019/06/28 jQuery
基于javascript实现碰撞检测
2020/03/12 Javascript
微信小程序视频弹幕发送功能的实现
2020/12/28 Javascript
用python找出那些被“标记”的照片
2017/04/20 Python
Python reduce()函数的用法小结
2017/11/15 Python
python中判断文件编码的chardet(实例讲解)
2017/12/21 Python
详解python tkinter教程-事件绑定
2019/03/28 Python
浅析Django中关于session的使用
2019/12/30 Python
高中生学习生活的自我评价
2013/11/27 职场文书
支教自我鉴定
2014/01/18 职场文书
法人委托书范本
2014/04/04 职场文书
小学教师师德师风自我评价
2015/03/04 职场文书
生日祝酒词大全
2015/08/10 职场文书
2016先进工作者事迹材料
2016/02/25 职场文书
新学期小学班主任工作计划
2019/06/21 职场文书
Python基本知识点总结
2022/04/07 Python
nginx location 带斜杠【 / 】与不带的区别
2022/04/13 Servers
Python函数对象与闭包函数
2022/04/13 Python