详解python metaclass(元类)


Posted in Python onAugust 13, 2020

元编程,一个听起来特别酷的词,强大的Lisp在这方面是好手,对于Python,尽管没有完善的元编程范式,一些天才的开发者还是创作了很多元编程的魔法。Django的ORM就是元编程的一个很好的例子。

本篇的概念和例子皆在Python3.6环境下

一切都是对象

Python里一切都是对象(object),基本数据类型,如数字,字串,函数都是对象。对象可以由类(class)进行创建。既然一切都是对象,那么类是对象吗?

是的,类也是对象,那么又是谁创造了类呢?答案也很简单,也是类,一个能创作类的类,就像上帝一样,开启了万物之始。这样的类,称之为元类(classmeta)。

类的定义

对象是通过类创建的,这个很好理解。例如下面的代码:

class Bar(object):
  pass

bar = Bar()
print(bar, bar.__class__)  # <__main__.Bar object at 0x101eb4630> <class '__main__.Bar'>
print(Bar, Bar.__class__) # <class '__main__.Bar'> <class 'type'>

可以看见对象 bar 是类 Bar 创建的实例。然而 Bar,看起来却是由一个叫 type 的类创建的实例。即 bar <-- Bar < -- type

上面的例子,对象是动态创建的,类则是通过关键字 class 声明定义的。class关键字背后的玄机是什么呢?

实际上,class Bar(object) 这样的代码,等价于 Bar = type('Bar', (objects, ), {})
即类 type 通过实例化创建了它的对象 Bar,而这个 Bar 恰恰是一个类。这样能创建类的类,就是 Python 的元类。

从创建 Bar 的代码上来看,元类 type 的 __init__ 方法有3个参数,

  • 第一个是创建的类的名字
  • 第二个是其继承父类的元类列表,
  • 最后就是一个属性字典,即该类所具有的属性。

type 元类

type是小写,因而很容易误以为它是一个函数。通过help(type)可以看到它的定义如下:

class type(object):
  """
  type(object_or_name, bases, dict)
  type(object) -> the object's type
  type(name, bases, dict) -> a new type
  """
  def __init__(cls, what, bases=None, dict=None): # known special case of type.__init__
    """
    type(object_or_name, bases, dict)
    type(object) -> the object's type
    type(name, bases, dict) -> a new type
    # (copied from class doc)
    """
    pass

   @staticmethod # known case of __new__
  def __new__(*args, **kwargs): # real signature unknown
    """ Create and return a new object. See help(type) for accurate signature. """
    pass

如前所述,__init__方法接受三个参数,type 实例化的过程,会创建一个新的类。创建类的代码来自 __new__ 方法,它的参数其实和 __init__,一样。至于它们之间有什么关系,后面再做介绍。目前只要知道,当调用 type 进行实例化的时候,会先自动调用 __new__ 方法,然后再接着调用 __init__方法,在类外面来看,最终会实例化一个对象,这个对象是一个类。

从 type 的定义来看,它继承 object,Python3的所有类,都继承来着 object,类type 也是 object 的实例,令人奇怪的是,object 既是类也是对象,它也是由 type实例化而来。有一种鸡生蛋,蛋生鸡的悖论。暂且先不管,只要知道所有类的顶级继承来自 object 就好。

自定义元类

既然元类可以创建类,那么自定义元类就很简单了,直接继承类 type 即可。先看下面一个例子:

class MyType(type):
  pass


class Bar(object, metaclass=MyType):
  pass


print(MyType, MyType.__class__) # <class '__main__.MyType'> <class 'type'>
print(Bar, Bar.__class__) # <class '__main__.Bar'> <class '__main__.MyType'>

可以看到,Bar在声明的时候,指定了其元类,此时的类 Bar 的__class__属性不再是 type,而是 MyType。即之前定义 Bar 的代码不再是 Bar = type('Bar', (objects, ), {}), 而是 Bar = MyType('Bar', (objects, ), {})。创建的元类的代码是MyType = type('MyType', (objects, ), {})

如果一个类没有显示的指定其元类,那么会沿着继承链寻找父类的元类,如果一直找不到,那么就使用默认的 type 元类。

元类冲突

每个类都可以指定元类,但是父类和子类的元类要是一条继承关系上的,否则会出现元类冲突。并且这个继承关系中,以继承最后面的元类为其元类。

元类的查找顺序大致为,先查看其继承的父类,找到父类的元类即停止。若直接父类没有元类,直到顶级父类 object ,此时父类(object)的元类是 type(basemetaclass),再看其自身有没有指定元类(submetaclass),如果指定了元类(submetaclass),再对比这个子元类(submetaclass)和父元类(basemetaclass),如果它们毫无继承关系,那么将会抛出元类冲突的错误。如果指定的子元类是父元类的父类,那么将会使用父元类,否则将使用期指定的子元类。

submetaclass <- basemetaclass使用 submetaclass 作为最终元类,
basemetaclass <- submetaclass, 使用 basemetaclass 作为最终元类,
两者无继承关系,抛出冲突。

有点像绕口令,且看代码例子

class MyType(type):
  pass

# 等价于 MyType = type('MyType', (object, ), {})

class Bar(object, metaclass=MyType):
  pass

# 等价于 Bar = MyType('Bar', (object, ), {})

class Foo(Bar):
  pass

# 等价于 Foo = MyType('Foo', (Foo, object, ), {})

print(Bar, Bar.__class__)  # <class '__main__.Bar'> <class '__main__.MyType'>
print(Foo, Foo.__class__) # <class '__main__.Foo'> <class '__main__.MyType'>

Bar的父元类(basemetaclass)type,指定子元类(submetaclass)是 MyType, MyType 继承自 type,所以Bar的元类是 MyType。

又如:

class MyType(type):
  pass


class Bar(object, metaclass=MyType):
  pass


class Foo(Bar, metaclass=type):
  pass


print(Bar, Bar.__class__)  # <class '__main__.Bar'> <class '__main__.MyType'>
print(Foo, Foo.__class__) # <class '__main__.Foo'> <class '__main__.MyType'>

尽管 Foo 也指定了元类(submetaclass) type,可是其父类的元类(basemetaclass)是 MyType, MyType
是 type的子类,因此 Foo的元类抛弃了指定的(submetaclass) type,而是沿用了其父类的MyType。

当 submetaclass 和 basemetaclass 没有继承关系的时候,将会元类冲突

class MyType(type):
  pass

class MyOtherType(type):
  pass

class Bar(object, metaclass=MyType):
  pass


class Foo(Bar, metaclass=MyOtherType):
  pass

运行代码,当定义的时候就会出现TypeError: metaclass conflict: the metaclass of a derived class must be a (non-strict)元类冲突的错误。

修改代码如下:

class MyType(type):
  pass

class MyOtherType(MyType):
  pass

class Bar(object, metaclass=MyType):
  pass


class Foo(Bar, metaclass=MyOtherType):
  pass


print(Bar, Bar.__class__) # <class '__main__.Bar'> <class '__main__.MyType'>
print(Foo, Foo.__class__) # <class '__main__.Foo'> <class '__main__.MyOtherType'>

可以看到 Bar 和 Foo 分别有自己的元类,并且都符合继承关系中寻找。再调换一下元类看看:

class MyType(type):
  pass

class MyOtherType(MyType):
  pass

class Bar(object, metaclass=MyOtherType):
  pass


class Foo(Bar, metaclass=MyType):
  pass


print(Bar, Bar.__class__) # <class '__main__.Bar'> <class '__main__.MyOtherType'>
print(Foo, Foo.__class__) # <class '__main__.Foo'> <class '__main__.MyOtherType'>

都使用了Foo还是使用了元子类作为元类。究其原因,其实也很好理解。定义父类的时候,使用了元类MyOtherType 。定义子类的时候,通过继承,找到了创建父类的元类,那么父类就是 MyOtherType 的实例。

如果使用 MyType 做为元类,那么他就是 MyType 的实例,MyType的实例会比MyOtherType具有的属性少,那么在继承链上,它又是 Bar的子类,这样看就是子类比父类还狭窄了,显然不是一个好的关系。即变成了下面的关系

Bar     <-  MyOtherType
 
  |           ↑
  |           |
  ↓           |

Foo     <-  MyType

因此当 MyType 是 MyOtherType的父类的时候,即使 Foo 指定了 MyType作为元类,还是会被忽略,使用其父元类MyOtherType。

上面的线的箭头要一直,才能使用各自指定的元类,否则使用箭头指向的那个类作为元类。元类没有继承关系,元类冲突。

对象(类)实例化

目前为止,我们了解了类的定义,即类是如何被元类创建出来的,但是创建的细节尚未涉及。即元类是如何通过实例化创建类的过程。这也是对象创建的过程。

前文介绍了一个对象是通过类创建的,类对象是通过元类创建的。创建类中,会先调用元类的__new__方法,设置其名称,继承关系和属性,返回一个实例。然后再调用实例的__init__方法进行初始化实例对象。

class MyType(type):

  def __init__(self, *args, **kwargs):
    print('init ', id(self), args, kwargs)

  def __new__(cls, *args, **kwargs):
    print('new', id(cls), args, kwargs)
    instance = super(MyType, cls).__new__(cls, *args, **kwargs)
    print(id(instance))
    return instance


class Bar(object, metaclass=MyType):
  pass

运行代码可以看见输出:

new 4323381304 ('Bar', (<class 'object'>,), {'__module__': '__main__', '__qualname__': 'Bar'}) {}
4323382232
init  4323382232 ('Bar', (<class 'object'>,), {'__module__': '__main__', '__qualname__': 'Bar'}) {}

注意,上面代码仅关注 Bar 类的创建,即 Bar =MyType('Bar', (object, ), {})这个定义代码。MyType进行实例化创建 Bar的过程中,会先用 其 __new__ 方法,后者调用了父类 type的 __new__方法,并返回了元类的实例, 同时调用这个实例的__init__方法,后者对改实例对象进行初始化。这也就是为什么方法名为 __init__

通常我们会在 __init__方法初始化一些实例对象的属性如果 __new__ 方法什么也不返回,那么 __init__ 方法是不会被调用的。

instance = super(MyType, cls).__new__(cls, *args, **kwargs), 有的地方也喜欢写成 type.__new__或者 type,前者是python中如何调用父类方法的问题,后者是直接使用type创建类的过程。比较推荐的写法还是使用 super 调用其父类的方法的方式。

类是元类的对象,普通类创建对象的过程,也是一样。因此,只要重写 __new__方法,还可以实现一个类还可以创建另外一个类的实例的魔法。

移花接木

重写 __new__ 方法,让其创建另外一个类的实例。

class Bar:
  def __init__(self, name):
    self.name = name
    print('Bar init')

  def say(self):
    print('say: Bar {}'.format(self.name))


class Foo(object):

  def __init__(self):
    print('self {}'.format(self))

  def __new__(cls, *args, **kwargs):
    instance = super(Foo, cls).__new__(Bar, *args, **kwargs)
    print('instance {}'.format(instance))
    instance.__init__('a class')
    return instance

  def say(self):
    print('say: Foo')


m = Foo()
print('m {}'.format(m))
m.say()

输出

instance <__main__.Bar object at 0x104033240>
Bar init
m <__main__.Bar object at 0x104033240>
say: Bar a class

在类 Foo 中,通过重写 __new__返回了一个 Bar 类的实例对象,然后调用 Bar 实例的 __inti__ 方法初始化,由于返回了 bar 实例,因此 Foo 的实例没有被创建,因此也不会调用它的实例方法 __inti__ 。这样就把 移花(Bar)接木(Foo)上了。

也许有人会觉得这样的诡异魔法有什么用呢?实际上,Tornado框架使用了这样的技术实现了一个叫 Configurable 的工厂类,用于创建不同网络IO下的epoll还是select模型。有兴趣可以参考其实现方式。

元类的应用

讨论了那么多原理的东西,最后肯定是要应用到实际中才有意义。既然类可以被动态的创建,那么很多定义在类的方法,岂不是也可以被动态的创建了呢。这样就省去了很多重复工作,也能实现酷酷的元编程。

元类可以创建单例模式,也可以用来实现 ORM,下面介绍的是Django使用元类实现的查找方式。更经典的model定义网上有很多例子,就不再介绍了。下面介绍一个model通过manger管理器实现查询方法的例子

import inspect


class QuerySet:

  def get(self, *args, **kwargs):
    print('get method')
    return self

  def filter(self, *args, **kwargs):
    print('filter method')
    return self


class BaseManager:

  def __init__(self):
    pass

  @classmethod
  def from_queryset(cls, queryset_class, class_name=None):
    if class_name is None:
      class_name = '%sFrom%s' % (cls.__name__, queryset_class.__name__)
    class_dict = {
      '_queryset_class': queryset_class,
    }
    class_dict.update(cls._get_queryset_methods(queryset_class))
    return type(class_name, (cls,), class_dict)

  def get_queryset(self):
    return self._queryset_class()

  @classmethod
  def _get_queryset_methods(cls, queryset_class):
    def create_method(name, method):
      def manager_method(self, *args, **kwargs):
        return getattr(self.get_queryset(), name)(*args, **kwargs)

      manager_method.__name__ = method.__name__
      manager_method.__doc__ = method.__doc__
      return manager_method

    new_methods = {}
    for name, method in inspect.getmembers(queryset_class, predicate=inspect.isfunction):
      if hasattr(cls, name):
        continue
      queryset_only = getattr(method, 'queryset_only', None)
      if queryset_only or (queryset_only is None and name.startswith('_')):
        continue
      new_methods[name] = create_method(name, method)
    return new_methods


class Manager(BaseManager.from_queryset(QuerySet)):
  pass


class ModelMetaClass(type):

  def __new__(cls, *args, **kwargs):
    name, bases, attrs = args
    attrs['objects'] = Manager()
    return super(ModelMetaClass, cls).__new__(cls, name, bases, attrs)


class Model(object, metaclass=ModelMetaClass):
  pass


class User(Model):
  pass


User.objects.get()
User.objects.filter()
User.objects.filter().get()

这样model就用使用期管理器Manger 下的方法了。通过model的元类ModelMetaClass,定义model的时候,就初始化了一个 Manger对象挂载到Model下面,而定义Manger的时候,也通过元类将QuerySet下的查询方法挂载到Manger下了。

总结

Python里一切都是对象,对象都是由类进行创建实例化而来。既然一切是对象,那么类也是对象,而类这种对象又是由一种更高级类创建而来,即所谓的元类。

元类可以创建类,Python默认的元类是 type。通过继承type,可以自定义元类,在自定义元类的时候定义或者重载 __new__,可以创建该类的实例对象,同时也可以修改类创建对象的行为。类通过 __new__创建实例对象,然后调用实例对象的 __init__初始化实例对象。

在使用自定义元类的时候,子类的的元类和父类的元类有关系,前者指定的元类必须和父类的元类是一个继承关系上的,否则会出现元类冲突。子类选取元类的取决于指定的元类和父元类的继承关系,子元类若是父元类的子类,则指定的元类为子元类,否则将会被忽略,使用父元类为其元类。

元类是元编程的一种技术手段,常用于实现工厂模式的策略。通过定义元类动态创建类和展开,可以实现很多设计精妙的应用。ORM 正式其中一种常用的方法。

以上就是详解python metaclass(元类)的详细内容,更多关于python metaclass(元类)的资料请关注三水点靠木其它相关文章!

Python 相关文章推荐
python基于socket实现网络广播的方法
Apr 29 Python
实例讲解Python中global语句下全局变量的值的修改
Jun 16 Python
python 读写中文json的实例详解
Oct 29 Python
python实现自动发送邮件发送多人、群发、多附件的示例
Jan 23 Python
用python实现对比两张图片的不同
Feb 05 Python
Python实现曲线拟合操作示例【基于numpy,scipy,matplotlib库】
Jul 12 Python
解决新django中的path不能使用正则表达式的问题
Dec 18 Python
python 五子棋如何获得鼠标点击坐标
Nov 04 Python
python 命名规范知识点汇总
Feb 14 Python
Windows系统下pycharm中的pip换源
Feb 23 Python
Python关键字及可变参数*args,**kw原理解析
Apr 04 Python
pandas中pd.groupby()的用法详解
Jun 16 Python
MAC平台基于Python Appium环境搭建过程图解
Aug 13 #Python
Expected conditions模块使用方法汇总代码解析
Aug 13 #Python
深入了解Python装饰器的高级用法
Aug 13 #Python
python高级特性简介
Aug 13 #Python
Pytest如何使用skip跳过执行测试
Aug 13 #Python
matplotlib基础绘图命令之bar的使用方法
Aug 13 #Python
Python logging模块原理解析及应用
Aug 13 #Python
You might like
php实现往pdf中加数字签名操作示例【附源码下载】
2018/08/07 PHP
jQuery 版本的文本输入框检查器Input Check
2009/07/09 Javascript
JavaScript 基础篇之对象、数组使用介绍(三)
2012/04/07 Javascript
推荐40款强大的 jQuery 导航插件和教程(上篇)
2012/09/14 Javascript
js 控制下拉菜单刷新的方法
2013/03/03 Javascript
open 动态修改img的onclick事件示例代码
2013/11/13 Javascript
利用js制作html table分页示例(js实现分页)
2014/04/25 Javascript
用box固定长宽实现图片自动轮播js代码
2014/06/09 Javascript
js中直接声明一个对象的方法
2014/08/10 Javascript
javascript设置和获取cookie的方法实例详解
2016/01/05 Javascript
AngularJS基础 ng-keypress 指令简单示例
2016/08/02 Javascript
JS获得多个同name 的input输入框的值的实现方法
2017/01/09 Javascript
微信小程序 连续旋转动画(this.animation.rotate)详解
2017/04/07 Javascript
javascript中new Array()和var arr=[]用法区别
2017/12/01 Javascript
jQuery实现鼠标响应式淘宝动画效果示例
2018/02/13 jQuery
Vue父组件调用子组件事件方法
2018/02/23 Javascript
详解angularjs4部署文件过大解决过程
2018/12/05 Javascript
layui之table checkbox初始化时选中对应选项的方法
2019/09/02 Javascript
Vue中使用wangeditor富文本编辑的问题
2021/02/07 Vue.js
详解Django的CSRF认证实现
2018/10/09 Python
python爬虫 模拟登录人人网过程解析
2019/07/31 Python
Python3.7+tkinter实现查询界面功能
2019/12/24 Python
pytorch模型预测结果与ndarray互转方式
2020/01/15 Python
家乐福巴西网上超市:Carrefour巴西
2016/10/31 全球购物
.NET方向面试题
2014/11/20 面试题
简述网络文件系统NFS,并说明其作用
2016/10/19 面试题
介绍一下Make? 为什么使用make
2013/12/08 面试题
用Java语言将一个键盘输入的数字转化成中文输出
2013/01/25 面试题
高一家长会邀请函
2014/01/12 职场文书
高三高考决心书
2014/03/11 职场文书
合作投资意向书
2014/04/01 职场文书
股权投资意向书
2014/04/01 职场文书
美化环境标语
2014/06/20 职场文书
2015年车间主任工作总结
2015/05/21 职场文书
初中化学教学反思
2016/02/22 职场文书
vue实现简易音乐播放器
2022/08/14 Vue.js